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:


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>


  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 



                TOP 1

                -1 BusinessID, 

                null BusinessName, 

                '' UserName



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


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

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


   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!




Cindy said...

Nerd. :)

Missa said...


ChrisS said...

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!