Wednesday, January 30, 2008

FormView Oddities and Using EditItemTemplate to Insert

Every now and then I come across those weird, annoying issues with ASP.NET which very few people seem to have had, and no one seems to have an answer.  It's especially frustrating because I can spend hours (I've spent 4 hours today tracking this one down) trying to figure out what's going on.  Maybe one of the gifts of being a programmer comes the ability to spend 4 hours researching, testing, and focusing on one problem.

In any case, we use FormViews pretty regularly in our ASP.NET applications.  For those who don't know, a FormView is very similar to the DataGrid and GridView controls.  You can place your form fields in either an ItemTemplate, EditItemTemplate or IntertItemTemplate.  This lets you bind the FormView to a DataSource and simple use Bind() to populate the values and update them back into the database.  We've been able to create pages with no, or very, very little code in the code behind using this method, and after a bit of a learning curve I definitely think this is the way to go for building sites.

Our only complaint is that you essentially duplicate form fields for each template.  For example, you may want to make the user name field read-only in the EditItemTemplate, but editable in the InsertItemTmplate. 

We (well, my wife anyway. . . Why are women always smarter than men?) came up with a solution where you can use the EditItemTemplate for both updates and inserts.  Essentially the SelectCommand will always return a row, even if the primary key value passed in is null/0.

So, we'll have a FormView for adding and editing builders which looks like this:

FormView

With the aspx code having:

   1:      <asp:FormView ID="fvContactInfo" runat="server" DataSourceID="odsContact" DataKeyNames="BusinessID,UserName"


   2:          DefaultMode="Edit" Width="100%">


   3:          <EditItemTemplate>


   4:              <%-- Business Name --%>


   5:              <div class="inputRow">


   6:                  <asp:Label ID="lblBusinessName" runat="server" Text="Business Name" CssClass="inputLabel"


   7:                      AssociatedControlID="txbBusinessName" />


   8:              </div>


   9:              <div class="inputRow">


  10:                  <asp:TextBox ID="txbBusinessName" runat="server" MaxLength="150" Text='<%#Bind("BusinessName") %>'


  11:                      CssClass="inputBox oneColumn" />


  12:                  <asp:RequiredFieldValidator ID="rqvBusinessName" ValidationGroup="CreateUserWizard1"


  13:                      runat="server" ControlToValidate="txbBusinessName" ErrorMessage="Business Name is required."


  14:                      Text="*" ToolTip="Business Name is required." />


  15:              </div>


  16:              <%-- Buttons --%>


  17:              <div class="inputRow" id="rowButtons" runat="server">


  18:                  <asp:Button ID="btnSave" runat="server" Text="Save" CommandName="Update" />


  19:                  <asp:Button ID="btnBack" runat="server" Visible="false" Text="Go back" />


  20:              </div>


  21:          </EditItemTemplate>


  22:      </asp:FormView>


  23:   


  24:  <%-- Data Source --%>


  25:  <asp:ObjectDataSource ID="odsContact" runat="server" SelectMethod="GetData" TypeName="BCDatasetTableAdapters.BusinessTableAdapter"


  26:      UpdateMethod="Update">


  27:      <SelectParameters>


  28:          <asp:Parameter Name="UserName" Type="String" />


  29:      </SelectParameters>


  30:      <UpdateParameters>


  31:          <asp:Parameter Name="BusinessName" Type="String" />


  32:          <asp:Parameter Name="UserName" Type="String" />


  33:          <asp:Parameter Name="BusinessID" Type="Int32" />


  34:      </UpdateParameters>


  35:  </asp:ObjectDataSource>




And a simple SQL procedure:




   1:  CREATE PROCEDURE [dbo].[sp_bc_BusinessSelect]


   2:  (


   3:      @UserName nvarchar(256)


   4:  )


   5:  AS


   6:      SET NOCOUNT ON;


   7:  BEGIN


   8:      IF @UserName is null 


   9:          BEGIN    


  10:              SELECT 


  11:                  TOP 1


  12:                  -1 BusinessID, 


  13:                  null BusinessName, 


  14:                  '' UserName


  15:              FROM


  16:                  bc_Business


  17:          END


  18:      ELSE


  19:          BEGIN


  20:              SELECT     


  21:                  TOP 1 


  22:                  IsNull(bc_Business.BusinessID, -1) BusinessID, 


  23:                  bc_Business.BusinessName


  24:                  bc_Business.UserName


  25:              FROM         


  26:                  bc_Business 


  27:              WHERE 


  28:                  UserName = @UserName


  29:              ORDER BY 


  30:                  BusinessName


  31:          END


  32:  END




While this is pretty straightforward, there is one problem which came up today.  When there are no builders at all in the builder table, the stored procedure will not return any rows (including the blank row your FormView expects).



That's really due to the fact that in lines 15 and 16 I specify a FROM table, though it's always possible that table is empty.  A better solution is to rewrite the query to look like this:




IF @UserName is null 


        BEGIN    


            SELECT 


                TOP 1


                -1 BusinessID, 


                null BusinessName, 


                '' UserName


        END


    ELSE



What's actually interesting is how the FormView handles the child controls in the Edit template when no data is returned.  When you attempt to run the following code:




   1:  Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load


   2:   


   3:      odsContact.SelectParameters("UserName").DefaultValue = UserName


   4:      CType(fvContactInfo.FindControl("lblBusinessName"), Label).Text = "Enter new business name here:"


   5:   


   6:  End Sub




You would expect line 4 to return either a reference to the label or, at worst, Nothing for the control, even if you place it in the FormViews DataBound or Unload event.



You actually receive an error that the FormView itself fvContactInfo threw a NullReferenceException.  Whenever you are binding a FormView to have the Edit mode handle both inserts and updates.



Accounting for the return of a blank row though, this is a great method to reduce the need to duplicate form fields in all templates.  The same template above which used Edit, Item and Insert templates would look similar to the following:




   1:  <asp:FormView ID="fvContactInfo" runat="server" DataSourceID="odsContact" DataKeyNames="BusinessID"


   2:      DefaultMode="Edit" Width="100%">


   3:      <EditItemTemplate>


   4:          Business Name:


   5:          <asp:TextBox ID="BusinessNameTextBox" runat="server" Text='<%# Bind("BusinessName") %>'>


   6:          </asp:TextBox><br />


   7:          User Name:


   8:          <asp:Label ID="UserNameLabel" runat="server" Text='<%# Bind("UserName") %>'>


   9:          </asp:Label><br />


  10:          <asp:LinkButton ID="UpdateButton" runat="server" CausesValidation="True" CommandName="Update"


  11:              Text="Update">


  12:          </asp:LinkButton>


  13:          <asp:LinkButton ID="UpdateCancelButton" runat="server" CausesValidation="False" CommandName="Cancel"


  14:              Text="Cancel">


  15:          </asp:LinkButton>


  16:      </EditItemTemplate>


  17:      <InsertItemTemplate>


  18:          Business Name:


  19:          <asp:TextBox ID="BusinessNameTextBox" runat="server" Text='<%# Bind("BusinessName") %>'>


  20:          </asp:TextBox><br />


  21:          User Name:


  22:          <asp:TextBox ID="UserNameTextBox" runat="server" Text='<%# Bind("User Name") %>'>


  23:          </asp:TextBox><br />


  24:          <asp:LinkButton ID="InsertButton" runat="server" CausesValidation="True" CommandName="Insert"


  25:              Text="Insert">


  26:          </asp:LinkButton>


  27:          <asp:LinkButton ID="InsertCancelButton" runat="server" CausesValidation="False" CommandName="Cancel"


  28:              Text="Cancel">


  29:          </asp:LinkButton>


  30:      </InsertItemTemplate>


  31:      <ItemTemplate>


  32:          BusinessName:


  33:          <asp:Label ID="BusinessNameLabel" runat="server" Text='<%# Bind("BusinessName") %>'>


  34:          </asp:Label><br />


  35:          UserName:


  36:          <asp:Label ID="UserNameLabel" runat="server" Text='<%# Bind("UserName") %>'></asp:Label><br />


  37:          <asp:LinkButton ID="EditButton" runat="server" CausesValidation="False" CommandName="Edit"


  38:              Text="Edit">


  39:          </asp:LinkButton>


  40:      </ItemTemplate>


  41:  </asp:FormView>




Which quickly becomes a nightmare to update.  Have fun!



Peace,

+Tom

3 comments:

Cindy said...

Nerd. :)

Missa said...

Ditto

ChrisS said...

Brother:
Good example, but u need a code formatter. Check out CopySourceAsHTML. That is the bomb for lifting your junk from VS.

Keep it fresh, I'm a reader!