Thursday, 31 July 2008

Dynamic Data Custom Pages Part 5: I18N? Internationalisation Custom Page

As far as I can see there are three (oops! four then and now a 5th) types of Custom Page:

  1. Custom Pages Part 1 - Standard Custom Page based on an existing PageTemplate and customised in the DynamicData\CustomPages folder.
  2. Custom Pages Part 2 - A completely Custom Page again in the DynamicData\CustomPages folder.
  3. Custom Pages Part 3 - Standard ASP.Net Page with Dynamic Data features added to take advantage of the FieldTemplates.
  4. Custom Pages Part 4 - A DetailsView and a GridView using Validation Groups
  5. Custom Pages Part 5 - I18N? Internationalisation Custom Page

When answering this thread here LCID: how to scaffold language dependent fields from separate tables... by Zoltán Lantos from Hungary, I knocked together this CustomPage and thought it was worth blogging about and so add a 5th post the my Three post series on Custom Pages.

The model

Figure 1 – the model

This works by filtering the LCID column by the users current culture, so there is a row in ProductDetails for each culture/language.

The requirements for this little project were:

  1. Admin to be able to see in a tabbed like layout all the ProductDetails foreach culture.
  2. The normal user to be able to see their ProductDetails in their own culture.

The FieldTemplates

Here are the FieldTemplates for editing and displaying the records culture here.

<%@ Control 
    Language="C#" 
    CodeFile="LCID_Edit.ascx.cs" 
    Inherits="LCID_EditField" %>

<asp:DropDownList 
    runat="server" 
    ID="DropDownList1" 
    CssClass="droplist" 
    ondatabound="DropDownList1_DataBound">
</asp:DropDownList>

Listing 1 – LCID_Edit.ascx

using System;
using System.Collections.Specialized;
using System.Globalization;
using System.Linq;
using System.Web.UI;
using System.Web.UI.WebControls;

public partial class LCID_EditField : System.Web.DynamicData.FieldTemplateUserControl
{
    protected override void OnDataBinding(EventArgs e)
    {
        base.OnDataBinding(e);

        // use linq to get a list of cultures to display in the drop down list
        var cultures = from c in CultureInfo.GetCultures(CultureTypes.NeutralCultures)
                       select new
                       {
                           Lcid = c.TwoLetterISOLanguageName,
                           Name = c.TwoLetterISOLanguageName + " - " + c.EnglishName
                       };

        // setup the drop down list
        DropDownList1.DataSource = cultures;
        DropDownList1.DataValueField = "Lcid";
        DropDownList1.DataTextField = "Name";
        DropDownList1.DataBind();
    }

    protected void DropDownList1_DataBound(object sender, EventArgs e)
    {
        // check the FieldValueString is not null
        if (String.IsNullOrEmpty(FieldValueString))
        {
            // set it to the culture of the client session
            DropDownList1.SelectedValue = CultureInfo.CurrentCulture.TwoLetterISOLanguageName;
        }
        else
        {
            // set the drop down list to the current vlaue
            DropDownList1.SelectedValue = FieldValueString;
        }
    }

    protected override void ExtractValues(IOrderedDictionary dictionary)
    {
        // get selected value
        dictionary[Column.Name] = ConvertEditedValue(DropDownList1.SelectedValue);
    }

    public override Control DataControl
    {
        get
        {
            return DropDownList1;
        }
    }
}

Listing 2 – LCID_Edit.ascx.cs code behind file

As you can see from the code above all we are doing is populating a DropDownList with all the cultures from CultureInfo.GetCultures of type CultureTypes.NeutralCultures using Linq smile_teeth to get them in an anonymous type.

And here is the much simpler LCID.ascx FieldTemplate:

protected override void OnDataBinding(EventArgs e)
{
    base.OnDataBinding(e);

    // use linq to get a list of cultures to display in the drop down list
    var culture = CultureInfo.GetCultures(CultureTypes.NeutralCultures).SingleOrDefault(c => c.TwoLetterISOLanguageName == FieldValueString);
    Label1.Text = culture.TwoLetterISOLanguageName + " - " + culture.EnglishName;
}

Listing 3 – LCID.ascx FieldTemplate OnDataBinding event handler

As you can see all the LCID.ascx is is a Label and the OnDataBinding event handler.

The Metadata and Partial Classes

I’m just going to paste them here and give detailed explanation as we go along
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;

public partial class ProductsDataContext
{
    // Implement the InsertProductDetail to do insert validation
    partial void InsertProductDetail(ProductDetail instance)
    {
        var DC = new ProductsDataContext();
        if (DC.ProductDetails.SingleOrDefault(pc => (pc.ProductId == instance.ProductId && pc.LCID == instance.LCID)) != null)
        {
            // Throw an exception if a match is found
            throw new ValidationException("Duplicate culture per Product is not permitted");
        }
        else
        {
            // finally send this to the DB
            this.ExecuteDynamicInsert(instance);
        }
    }
}

[MetadataType(typeof(ProductDetailMD))]
public partial class ProductDetail
{
    public class ProductDetailMD
    {
        [UIHint("LCID")]
        public object LCID { get; set; }
    }
}

Listing 4 – Metadata and Partial classes

As you can see we two parts to the ProductsMD.cs file; the first part check for duplicate languages/culture on a product and the second part adds the UIHint to the LCID column.

The Products Edit page

Here’s a snapshot of the finished page:

Screen shot of the finished page

Figure 1 - Screen shot of the finished page

There are three part to the page;

  1. A DetailsView for the Product
  2. A ListView for the “tab” control (it could be styled to look like tabs using css if you wanted to)
  3. A FromView and GridvView for the ProductDetails

1. A DetailsView for the Product 

Is strait forward enough and I started with a normal Edit.aspx Page Template and then added all the other components.

2. A ListView for the “tab” control

To do this at first I thought of using the Ajax Toolkit Tab control and embedding it in a ListView but that was a no go as you could not break apart the individual part between different ListView templates. So I finally opted for a ListView with LinkButtons as Select commands, each row only has a Select button in it.

3. A FromView and GridvView for the ProductDetails

The FormView is used for inserting and the GridView is used for editing and displaying ProductDetails.

So here’s the code for the page and the code behind.

<%@ Page Language="C#" MasterPageFile="~/Site.master" CodeFile="Edit.aspx.cs" Inherits="Edit" %>

<asp:Content ID="Content1" ContentPlaceHolderID="ContentPlaceHolder1" runat="Server">

    <asp:DynamicDataManager 
        runat="server" 
        ID="DynamicDataManager1" 
        AutoLoadForeignKeys="true" />
        
    <h2>Edit entry from table <%= mtProducts.DisplayName %></h2>
    
    <asp:ScriptManagerProxy 
        runat="server" 
        ID="ScriptManagerProxy1" />
    
    <asp:UpdatePanel 
        runat="server" 
        ID="UpdatePanel1">
    
        <ContentTemplate>
            <asp:ValidationSummary 
                runat="server" 
                ID="ValidationSummary1" 
                EnableClientScript="true"
                HeaderText="List of validation errors" />
                
            <asp:DynamicValidator 
                runat="server" 
                ID="DetailsViewValidator" 
                ControlToValidate="DetailsView1"
                Display="None" />
                
            <asp:DetailsView 
                runat="server" 
                ID="DetailsView1" 
                DataSourceID="ldsProducts"
                DefaultMode="Edit" 
                AutoGenerateEditButton="True" 
                OnItemCommand="DetailsView1_ItemCommand"
                OnItemUpdated="DetailsView1_ItemUpdated" 
                CssClass="detailstable" 
                FieldHeaderStyle-CssClass="bold" 
                AutoGenerateRows="False" 
                DataKeyNames="Id">
                <FieldHeaderStyle CssClass="bold" />
                <Fields>
                    <asp:DynamicField DataField="Code" />
                    <asp:DynamicField DataField="Display" />
                    <asp:DynamicField DataField="Inserted" />
                </Fields>
            </asp:DetailsView>
            
            <asp:LinqDataSource 
                runat="server" 
                ID="ldsProducts" 
                ContextTypeName="ProductsDataContext" 
                TableName="Products" 
                EnableUpdate="True">
                <WhereParameters>
                    <asp:DynamicQueryStringParameter />
                </WhereParameters>
            </asp:LinqDataSource>
            
            <h2>Cultures</h2>

            <asp:ListView 
                runat="server" 
                ID="lvLanguages" 
                DataSourceID="ldsLanguages" 
                DataKeyNames="LCID" 
                OnItemCommand="lvLanguages_ItemCommand">
                
                <LayoutTemplate>
                    <div>
                        <span id="itemPlaceHolder" runat="server"></span>
                        <asp:LinkButton 
                            runat="server" 
                            ID="lvLanguagesInsert" 
                            CommandName="InsertProductDetail" 
                            CommandArgument="LCID" 
                            CausesValidation="false">
                            add new
                        </asp:LinkButton>
                        <asp:Label 
                            runat="server"
                            ID="InsertLabel" 
                            Visible="false"> 
                            <strong>add new</strong>
                        </asp:Label>
                    </div>
                </LayoutTemplate>
                
                <ItemTemplate>
                    <asp:LinkButton 
                        runat="server" 
                        Text='<%# Eval("LCID") %>' 
                        CommandName="Select" 
                        CausesValidation="false">
                    </asp:LinkButton>
                </ItemTemplate>
                
                <SelectedItemTemplate>
                 <strong><%# Eval("LCID") %></strong>
                </SelectedItemTemplate>
                
            </asp:ListView>
            
            <asp:LinqDataSource 
                runat="server" 
                ID="ldsLanguages" 
                ContextTypeName="ProductsDataContext" 
                TableName="ProductDetails" 
                Where="ProductId == @ProductId" 
                GroupBy="LCID" 
                Select="new (key as LCID, it as ProductDetails)" 
                OrderGroupsBy="key">
                <WhereParameters>
                    <asp:ControlParameter 
                        ControlID="DetailsView1" 
                        Name="ProductId" 
                        PropertyName="SelectedValue" 
                        Type="Int32" />
                </WhereParameters>
            </asp:LinqDataSource>
            
            <br /><br />
            
            <asp:DynamicValidator 
                runat="server" 
                ID="GridViewDynamicValidator" 
                ControlToValidate="GridView1"
                Display="None" />
                
            <asp:GridView 
                runat="server" 
                ID="GridView1" 
                AutoGenerateColumns="False" 
                CssClass="gridview" 
                DataKeyNames="Id" 
                DataSourceID="ldsProductDetails" 
                OnRowDeleted="GridView1_RowDeleted">
                <Columns>
                    <asp:CommandField 
                        ShowEditButton="True" 
                        ShowDeleteButton="True" />
                    <asp:DynamicField DataField="LCID" />
                    <asp:DynamicField DataField="Type" />
                    <asp:DynamicField DataField="Price" />
                    <asp:DynamicField DataField="Description" />
                </Columns>
            </asp:GridView>
            
            <asp:DynamicValidator 
                runat="server" 
                ID="ProductDetailDynamicValidator" 
                ControlToValidate="fvProductDetail"
                ValidationGroup="ProductDetailsList_Insert"
                Display="None" />
                
            <asp:FormView 
                runat="server"
                ID="fvProductDetail"
                CssClass="gridview"
                DefaultMode="Insert" 
                Visible="false"
                DataSourceID="ldsProductDetails" 
                OnItemInserted="fvProductDetail_ItemInserted" 
                onitemcommand="fvProductDetail_ItemCommand">
                
                <InsertItemTemplate>
                        <thead>
                            <tr>
                                <th>
                                </th>
                                <th>
                                    LCID
                                </th>
                                <th>
                                    Type
                                </th>
                                <th>
                                    Price
                                </th>
                                <th>
                                    Description
                                </th>
                            </tr>
                        </thead>
                        <tbody>
                            <tr>
                                <td>
                                    <asp:LinkButton 
                                        runat="server" 
                                        ID="InsertLinkButton" 
                                        CommandName="Insert">
                                        Insert
                                    </asp:LinkButton>
                                    <asp:LinkButton 
                                        runat="server" 
                                        ID="CancelLinkButton" 
                                        CausesValidation="false" 
                                        CommandName="Cancel">
                                        Cancel
                                    </asp:LinkButton>
                                </td>
                                <td>
                                    <asp:DynamicControl 
                                        ID="DynamicControl1" 
                                        DataField="LCID" 
                                        Mode="Insert" 
                                        runat="server" />
                                </td>
                                <td>
                                    <asp:DynamicControl 
                                        ID="DynamicControl2" 
                                        DataField="Type" 
                                        Mode="Insert" 
                                        runat="server" />
                                </td>
                                <td>
                                    <asp:DynamicControl 
                                        ID="DynamicControl3" 
                                        DataField="Price" 
                                        Mode="Insert" 
                                        runat="server" />
                                </td>
                                <td>
                                    <asp:DynamicControl 
                                        ID="DynamicControl4" 
                                        DataField="Description" 
                                        Mode="Insert" 
                                        runat="server" />
                                </td>
                            </tr>
                        </tbody>
                </InsertItemTemplate>
            </asp:FormView>
            
            <asp:LinqDataSource 
                runat="server" 
                ID="ldsProductDetails" 
                ContextTypeName="ProductsDataContext" 
                TableName="ProductDetails" 
                Where="LCID == @LCID &amp;&amp; ProductId == @ProductId" 
                EnableDelete="True" 
                EnableInsert="True" 
                EnableUpdate="True" 
                OnInserting="ldsProductDetails_Inserting">
                <WhereParameters>
                    <asp:ControlParameter 
                        ControlID="lvLanguages" 
                        Name="LCID" 
                        PropertyName="SelectedValue" 
                        Type="String" />
                    <asp:ControlParameter 
                        ControlID="DetailsView1" 
                        Name="ProductId" 
                        PropertyName="SelectedValue" 
                        Type="Int32" />
                </WhereParameters>
            </asp:LinqDataSource>
            
        </ContentTemplate>
    </asp:UpdatePanel>
</asp:Content>

Listing 5 – Edit.aspx Products page

using System;
using System.Web.DynamicData;
using System.Web.UI.WebControls;

public partial class Edit : System.Web.UI.Page
{
    protected MetaTable mtProducts;
    protected MetaTable mtProductDetails;

    protected void Page_Init(object sender, EventArgs e)
    {
        DynamicDataManager1.RegisterControl(DetailsView1, true);
        DynamicDataManager1.RegisterControl(GridView1);
        DynamicDataManager1.RegisterControl(fvProductDetail);
    }

    protected void Page_Load(object sender, EventArgs e)
    {
        mtProducts = ldsProducts.GetTable();
        mtProductDetails = ldsProductDetails.GetTable();
        Title = mtProducts.DisplayName;

        // set the first item to be selected
        lvLanguages.SelectedIndex = 0;
    }

    protected void DetailsView1_ItemCommand(object sender, DetailsViewCommandEventArgs e)
    {
        // redirect to the List page when cancel clicked
        if (e.CommandName == DataControlCommands.CancelCommandName)
        {
            Response.Redirect(mtProducts.ListActionPath);
        }
    }

    protected void DetailsView1_ItemUpdated(object sender, DetailsViewUpdatedEventArgs e)
    {
        // redirect to the List page when update clicked
        if (e.Exception == null || e.ExceptionHandled)
        {
            Response.Redirect(mtProducts.ListActionPath);
        }
    }

    protected void lvLanguages_ItemCommand(object sender, ListViewCommandEventArgs e)
    {
        // toggel the view between Insert and Edit/Display
        var insertButton = (LinkButton)lvLanguages.FindControl("lvLanguagesInsert");
        var insertLabel = (Label)lvLanguages.FindControl("InsertLabel");
        if (e.CommandName == "InsertProductDetail")
        {
            GridView1.Visible = false;
            lvLanguages.SelectedIndex = -1;
            fvProductDetail.Visible = true;
            insertButton.Visible = false;
            insertLabel.Visible = true;
        }
        if (e.CommandName == "Select")
        {
            GridView1.Visible = true;
            lvLanguages.SelectedIndex = 0;
            fvProductDetail.Visible = false;
            insertButton.Visible = true;
            insertLabel.Visible = false;
        }
    }

    protected void GridView1_RowDeleted(object sender, GridViewDeletedEventArgs e)
    {
        // when an item is deleted the redirect back 
        // to the same page with the current id
        if (e.Exception == null || e.ExceptionHandled)
        {
            var s = DetailsView1.DataKey.Value.ToString();
            var s1 = mtProducts.GetActionPath(PageAction.Edit) + @"?Id=" + s;
            Response.Redirect(s1);
        }
    }

    protected void fvProductDetail_ItemInserted(object sender, FormViewInsertedEventArgs e)
    {
        // when an item is inserted the redirect back 
        // to the same page with the current id
        if (e.Exception == null || e.ExceptionHandled)
        {
            var s = DetailsView1.DataKey.Value.ToString();
            var s1 = mtProducts.GetActionPath(PageAction.Edit) + @"?Id=" + s;
            Response.Redirect(s1);
        }
    }

    protected void fvProductDetail_ItemCommand(object sender, FormViewCommandEventArgs e)
    {
        // on the cancel button being presed return the page normal state
        var insertButton = (LinkButton)lvLanguages.FindControl("lvLanguagesInsert");
        var insertLabel = (Label)lvLanguages.FindControl("InsertLabel");
        if (e.CommandName == "Cancel")
        {
            GridView1.Visible = true;
            lvLanguages.SelectedIndex = 0;
            fvProductDetail.Visible = false;
            insertButton.Visible = true;
            insertLabel.Visible = false;
        }
    }

    protected void ldsProductDetails_Inserting(object sender, LinqDataSourceInsertEventArgs e)
    {
        // add the ProductId from the DetailsView
        ((ProductDetail)e.NewObject).ProductId = (int)DetailsView1.DataKey.Value;
    }
}

Listing 6 – Edit.aspx.cs code behind for ProductDetails

The comments should explain what is happening.

Thanks for reading smile_teeth

Note: That requirement number 2 should be trivial in that all you need is a custom page with a DetailView and a FormView and adding a WHERE parameter to the LinqDataSource limiting the data to the clients culture.

UPDATE

I’ve just had a thought and decided to add it here :D

I’ve re done the ListView Tabs and added some tool tips:

<asp:ListView 
    runat="server" 
    ID="lvLanguages" 
    DataSourceID="ldsLanguages" 
    DataKeyNames="LCID" 
    OnItemCommand="lvLanguages_ItemCommand">
    
    <LayoutTemplate>
        <div>
            <span id="itemPlaceHolder" runat="server"></span>
            <asp:LinkButton 
                runat="server" 
                ID="lvLanguagesInsert"
                ToolTip="Insert new Product Decription"
                CommandName="InsertProductDetail" 
                CommandArgument="LCID" 
                CausesValidation="false">
                add new
            </asp:LinkButton>
            <asp:Label 
                runat="server"
                ID="InsertLabel" 
                ToolTip="Insert new Product Decription"
                Visible="false"> 
                <strong>add new</strong>
            </asp:Label>
        </div>
    </LayoutTemplate>
    
    <ItemTemplate>
        <asp:LinkButton 
            runat="server" 
            Text='<%# Eval("LCID") %>' 
            CommandName="Select" 
            ToolTip='<%# Eval("Name") %>'
            CausesValidation="false">
        </asp:LinkButton>
    </ItemTemplate>
    
    <SelectedItemTemplate>
     <strong title='<%# Eval("Name") %>'><%# Eval("LCID") %></strong>
    </SelectedItemTemplate>
    
</asp:ListView>

<asp:LinqDataSource 
    runat="server" 
    ID="ldsLanguages"
    OnSelecting="ldsLanguages_Selecting">
    <WhereParameters>
    </WhereParameters>
</asp:LinqDataSource>

Listing 7 – Update ListView and associated LinqDataSource

protected void ldsLanguages_Selecting(object sender, LinqDataSourceSelectEventArgs e)
{
    var DC = new ProductsDataContext();

    var productCultures = from pdc in DC.ProductDetails
                          where pdc.ProductId == (int)DetailsView1.DataKey.Value
                          select pdc;

    var cultureDetails = from pd in productCultures.ToArray()
                         join pc in CultureInfo.GetCultures(CultureTypes.NeutralCultures) on 
                            pd.LCID equals pc.TwoLetterISOLanguageName
                         select new
                         {
                             LCID = pd.LCID,
                             Name = pc.EnglishName
                         };

    e.Result = cultureDetails;
}

Listing 8 – Selecting event handler for the ldsLanguages LinqDataSource

What I’ve done is taken the data from the SQL tables and merged it with the CultureInfo to produce a list of cultures plus their English names. Note I’ve removed the WHERE parameters etc from the LinqDataSource and added the OnSelecting event handler.

Yet another neat use of Linq smile_teeth Yes I could have done a separate article on this but I though it fitted well here.

Monday, 28 July 2008

Dynamic Data and Field Templates - An Advanced FieldTemplate

  1. The Anatomy of a FieldTemplate.
  2. Your First FieldTemplate.
  3. An Advanced FieldTemplate.
  4. A Second Advanced FieldTemplate.
  5. An Advanced FieldTemplate with a GridView.
  6. An Advanced FieldTemplate with a DetailsView.
  7. An Advanced FieldTemplate with a GridView/DetailsView Project

Well I say advanced... well here we go we are going to use the AutoCompleteFilter web service from Dynamic Data Futures example to make an AutoCompleteText_Edit FieldTemplate. What this will do is look up values in the column that we are editing for matches i.e. if we had a column for category names and we typed in con we may get list like Condiments, Confections, Containers etc.

Note: This does NOT work with foreign key fields it’s a text field only thing

I’ll be working with file based website project so there will be changes to make when we use the bits from the Dynamic Data Futures.

Step 1 – adding the AutocompleteFilter.asmx files and AutocompleteStyle.css

Copy the AutocompleteFilter.asmx and AutocompleteStyle.css to the root of the website and copy the AutocompleteFilter.asmx.cs to the App_Code folder.

Now we need to edit the AutocompleteFilter.asmx file:

<%@ WebService 
    Language="C#" 
    CodeBehind="~/AutocompleteFilterService.cs" 
    Class="DynamicDataFuturesSample.AutocompleteFilterService" %>

Listing 1 – the AutocompleteFilter.asmx

We need to remove the DynamicDataFuturesSample. namespace from the Class="DynamicDataFuturesSample.AutocompleteFilterService" so it reads Class="AutocompleteFilterService".

namespace DynamicDataFuturesSample
{

Listing 2 – namespace from the AutocompleteFilter.asmx.cs

Now remove the surrounding namespace from the AutocompleteFilter.asmx.cs file including the last “}” brace.

You can save and close both files.

Now to add the AutocompleteFilter style sheet to the master page and add the AutocompleteStyle.css style sheet to the head section of the page

<head runat="server">
    <title>Dynamic Data Site</title>
    <link href="~/Site.css" rel="stylesheet" type="text/css" />
    <link href="~/AutocompleteStyle.css" rel="stylesheet" type="text/css" />
</head>

Listing 3 – head section of the Site.master page with the AutocompleteStyle.css style sheet added

Step 2 – Adding a reference to the Dynamic Data Futures

You can achieve this in one of two ways

  1. Add a reference to the DLL created when the Dynamic Data Futures was compiled. Right mouse click on the root of the website and choose Add Reference and then click the browse tab and browse to the DLL and click OK.
  2. From the File menu add –> Existing Project and choose the Dynamic Data Futures project. When added, choose add reference from the root of the website and select the projects tab, choose Dynamic Data Futures project and click OK (useful when debugging).

Step 3 – Converting the Autocomplete.ascx from Daynamic Data Futures

Copy the Autocomplete.ascx and Autocomplete.ascx.cs files from the ~/DynamicData/Filter folder in the DynamicDataFuturesSample website to the ~/DynamicData/FieldTemplates folder in our website and then rename to TextAutocomplete_Edit.

Now as in step 1 we need to remove the namespace from the copied files:

<%@ Control 
    Language="C#" 
    AutoEventWireup="true"
    Inherits="DynamicDataFuturesSample.Autocomplete_Filter" 
    Codebehind="Autocomplete.ascx.cs" %>

Listing 4 – Control  tag of Autocomplete.ascx file

So remove the DynamicDataFuturesSample. from the Inherits property but also this time rename the Codebehind to CodeFile and change the name of the class and CodeFile/Codebehind see below

<%@ Control 
    Language="C#" 
    AutoEventWireup="true"
    Inherits="TextAutocomplete_EditField" 
    CodeFile="TextAutocomplete_Edit.ascx.cs" %>

Listing 5 – Control  tag of TextAutocomplete_Edit.ascx file

namespace DynamicDataFuturesSample
{
    public partial class Autocomplete_Filter : FilterUserControlBase
    {

Listing 6 - TextAutocomplete_Edit.ascx.cs file

Change Listing 6 to look like Listing 7 remembering to remove the closing brace “}” of the namespace.

public partial class TextAutocomplete_EditField : FieldTemplateUserControl
{

Listing 7 – Altered TextAutocomplete_Edit.ascx.cs file

Note the change of FilterUserControlBase to FieldTemplateUserControl.

Now go to the bottom of the TextAutocomplete_Edit.ascx file.

<script type="text/javascript">
    // Work around browser behavior of "auto-submitting" simple forms
    var frm = document.getElementById("aspnetForm");
    if (frm) {
        frm.onsubmit = function() { return false; };
    }
</script>
<%-- Prevent enter in textbox from causing the collapsible panel from operating --%>
<input type="submit" style="display:none;" />

Listing 8 – End of the TextAutocomplete_Edit.ascx

You will need to remove script altogether as it stops the LinkButtons on the DetailsView from posting back.

<%-- Prevent enter in textbox from causing the collapsible panel from operating –%>
<input type="submit" style="display:none;" />

Listing 9 – Modified end of the TextAutocomplete_Edit.ascx

Remove the Clear button from the user control

<asp:TextBox runat="server" ID="AutocompleteTextBox" Width="300" autocomplete="off"  />
<asp:HiddenField runat="server" ID="AutocompleteValue" />
<asp:Button runat="server" ID="ClearButton" OnClick="ClearButton_Click" Text="Clear" />
<ajaxToolkit:AutoCompleteExtender

Listing 10 – Remove the Clear Button
<asp:TextBox runat="server" ID="AutocompleteTextBox" Width="300" autocomplete="off"  />
<asp:HiddenField runat="server" ID="AutocompleteValue" />
<ajaxToolkit:AutoCompleteExtender

Listing 11 – Clear Button Removed

You can save and close the TextAutocomplete_Edit.ascx file

Now to hack the rest of the code in the code behind file.

Delete the following from the end of the control class:

public void ClearButton_Click(object sender, EventArgs e)
{
    // this would probably be better handled using client javascirpt
    AutocompleteValue.Value = String.Empty;
    AutocompleteTextBox.Text = String.Empty;
}

public override string SelectedValue
{
    get
    {
        return String.IsNullOrEmpty(AutocompleteValue.Value) ? null : AutocompleteValue.Value;
    }
}

Listing 12 – bits to remove from the end of the code behind

Now to finish the hacking :D

Remove the following from the Page_Init event handler

if (!Page.IsPostBack && !String.IsNullOrEmpty(InitialValue))
{
    // set the initial value of the filter if it's present in the request URL

    MetaTable parentTable = fkColumn.ParentTable;
    IQueryable query = parentTable.GetQuery();
    // multi-column PK values are seperated by commas
    var singleCall = LinqExpressionHelper.BuildSingleItemQuery(query, parentTable, InitialValue.Split(','));
    var row = query.Provider.Execute(singleCall);
    string display = parentTable.GetDisplayString(row);

    AutocompleteTextBox.Text = display;
    AutocompleteValue.Value = InitialValue;
}

Listing 13 – delete from Page_Init

Edit the remaining code to look like this adding the ExtractValues and DataControl methods and the OnDataBinding event handler:

public partial class TextAutocomplete_EditField : FieldTemplateUserControl
{
    protected void Page_Init(object sender, EventArgs e)
    {
        autoComplete1.ContextKey = AutocompleteFilterService.GetContextKey(Table);

        // modify behaviorID so it does not clash with other autocomplete extenders on the page
        autoComplete1.Animations = autoComplete1.Animations.Replace(autoComplete1.BehaviorID, AutocompleteTextBox.UniqueID);
        autoComplete1.BehaviorID = AutocompleteTextBox.UniqueID;
    }

    protected override void OnDataBinding(EventArgs e)
    {
        base.OnDataBinding(e);

        if (!String.IsNullOrEmpty(FieldValueString))
        {
            // set the initial value 
            AutocompleteTextBox.Text = FieldValueString;
        }
    }

    protected override void ExtractValues(System.Collections.Specialized.IOrderedDictionary dictionary)
    {
        dictionary[Column.Name] = AutocompleteTextBox.Text;
    }

    public override System.Web.UI.Control DataControl
    {
        get
        {
            return AutocompleteTextBox;
        }
    }
}

Listing 14 – Completed TextAutocomplete_Edit.ascx.cs

Use UIHint attribute to tell Dynamic Data to use TextAutocomplete for that field and run it hers what it should look like:

TextAutocomplete_Edit in action

Figure 1 – TextAutocomplete_Edit in action

Step 4 – Adding an Attribute

Here we will add an attribute to set the minimum number of characters typed before the auto complete is started (the default is 1 see MinimumPrefixLength in the TextAutocomplete_Edit.ascx file). Here’s the code for the attribute:

using System;

/// <summary>
/// Summary description for EnumerationTypeAttribute
/// </summary>
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public class TextAutocompleteAttribute : Attribute
{
    public TextAutocompleteAttribute(int prefixLength)
    {
        AutocompletePrefixLength = prefixLength;
    }

    public int AutocompletePrefixLength { get; set; }
}

Listing 15 – TextAutoCompleteAttribute

The code in Listing 15 has only one property AutocompletePrefixLength which we will use to set the MinimumPrefixLength property of the Ajax Toolkit AutoCompleteExtender.

Edit the TextAutocomplete_Edit.ascx.cs’s Page_Init event handler so it looks like this:

 

protected void Page_Init(object sender, EventArgs e)
{
    // get the AutocompletePrefixLength if present
    var prefixLength = Column.Attributes.OfType<TextAutocompleteAttribute>().SingleOrDefault();

    // if present set MinimumPrefixLength
    if (prefixLength != null)
        autoComplete1.MinimumPrefixLength = prefixLength.AutocompletePrefixLength;

    autoComplete1.ContextKey = AutocompleteFilterService.GetContextKey(Table);

    // modify behaviorID so it does not clash with other autocomplete extenders on the page
    autoComplete1.Animations = autoComplete1.Animations.Replace(autoComplete1.BehaviorID, AutocompleteTextBox.UniqueID);
    autoComplete1.BehaviorID = AutocompleteTextBox.UniqueID;
}

Listing 16 – adding the code to read the attribute

The first line of code gets the attribute and then the IF statement tests to se if the value is not null and if so sets the AutoCompleteExtenders MinimumPrefixLength to it’s value.

That’s it on this one, until I get the time to write a FieldTemplate to handle SQL Server 2008’s Geography data type via Virtual Earth.

Tuesday, 22 July 2008

Dynamic Data and Field Templates - Your First FieldTemplate *** UPDATED ***

  1. The Anatomy of a FieldTemplate.
  2. Your First FieldTemplate.
  3. An Advanced FieldTemplate.
  4. A Second Advanced FieldTemplate.
  5. An Advanced FieldTemplate with a GridView.
  6. An Advanced FieldTemplate with a DetailsView.
  7. An Advanced FieldTemplate with a GridView/DetailsView Project.

Your First FieldTemplate

We’ll create a simple FieldTemplate that displays an enumeration as RadioButtonList.

Create Database and Table

We will need a database to run against so in Solution Explorer create a new ASP.Net folder App_Data in the root of the website and create a new Database in there called Animals. Now create a new table called Pets with the following columns.

Pets Table

Figure 1 – Pets Table

Create the Enumeration Type

/// <summary>
/// My not very exhaustive set of animals
/// </summary>
public enum AnimalType
{
    Budgie,
    Cat,
    Dog,
    Gerbil,
    Hamster,
    Lizard,
    Mouse,
    Parrot,
    Rat,
    Snake
}
public enum Gender // Added
{
    Male = 0,
    Female = 1
}

Listing 1 – AnimalType & Gender ***UPDATED***

Create the Model

Add a new Linq to SQL to the App_Code folder and and call it Animals. Copy the Pets table to it. Now we need to set the type of the Pets.Type to AnimalTypes and also do the same for the sex column setting its type to Gender.

Setting the Pets.Type to AnimalType enum

Figure 2 - Setting the Pets.Type to the AnimalType enum

Save the and Close the Model.

In the same class file as the enums add your metadata.

using System.ComponentModel.DataAnnotations;
using System.ComponentModel;

[MetadataType(typeof(PetMetaData))]
public partial class Pet
{
    public class PetMetaData
    {
        [UIHint("Enumeration")]
        public object Animal { get; set; }

        [UIHint("Enumeration")]
        public object sex { get; set; }
    }
}

Listing 2 – Pet metadata ***UPDATED***

Creating a New FieldTemplate

Expand the DynamicData folder and right-click on the FieldTemplates folder, choose Add New Item… and choose Web User Control from the list of items. Give it the name Enumeration_Edit.ascx

Change the base class that the user control inherits from System.Web.UI.UserControl to System.Web.DynamicData.FieldTemplateUserControl. and a new using System.Web.DynamicData. Change the class name to Enumeration_EditField from DynamicData_FieldTemplates_Enumeration_Edit in both the ascx and code behind files.

<%@ Control Language="C#"
    AutoEventWireup="true"
    CodeFile="Enumeration.ascx.cs"
    Inherits="Enumeration_EditField" %>

Listing 3 – Enumeration_Edit.ascx

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.DynamicData;

public partial class Enumeration_EditField : System.Web.DynamicData.FieldTemplateUserControl
{

}

Listing 4 – Enumeration_Edit.ascx.cs code behind

The UserControl is now ready for us to add our code to make it show a DropDownList control populated with AnimalTypes.

Building Our Custom FieldTemplate

Add a DropDownList to the UserControl leave it as DropDownList1 and switch to the code behind.

In the code behind body of the class type protected override On and as you type On you will notice the auto-complete intellisense offer you some options use the arrow keys to select OnDataBinding and hit the tab key (but not too hard or you will break your keyboard :D).

‌auto-complete intellisense

Figure 3 - Auto-Complete Intellisense

Now you should have:

public partial class Enumeration_EditField : System.Web.DynamicData.FieldTemplateUserControl
{
    protected override void OnDataBinding(EventArgs e)
    {
        base.OnDataBinding(e);
    }
}

Listing 5 – OnDataBinding event handler

I mentioned the above because I think it’s really neat smile_teeth.

Note: When overriding OnDataBinding be sure to call the base class's OnDataBinding method so that registered delegates receive the event.

After the base.OnDataBinding(e); line we need to add our code to populate the DropDownList.

<%@ Control Language="C#"
    AutoEventWireup="true"
    CodeFile="Enumeration_Edit.ascx.cs"
    Inherits="Enumeration_EditField" %>
   
<asp:DropDownList
    runat="server"
    ID="DropDownList1"
    CssClass="droplist"
    OnDataBound="DropDownList1_DataBound">
</asp:DropDownList>

Listing 6 – Finished Enumeration_Edit.ascx note the OnDataBound

using System;
using System.Web.UI;
using System.Web.UI.WebControls;

public partial class Enumeration_EditField : System.Web.DynamicData.FieldTemplateUserControl
{
    protected override void OnDataBinding(EventArgs e)
    {
        // When overriding OnDataBinding be sure to call the base class's
        // OnDataBinding method so that registered delegates receive the event.
        base.OnDataBinding(e);

        // get a data bindable list of permissions for the DDL
        var enumList = Enum.GetValues(Column.ColumnType);

        DropDownList1.DataSource = enumList;
        DropDownList1.DataBind();
    }

    protected override void ExtractValues(System.Collections.Specialized.IOrderedDictionary dictionary)
    {
        dictionary[Column.Name] = ConvertEditedValue(DropDownList1.SelectedValue);
    }

    protected void DropDownList1_DataBound(object sender, EventArgs e)
    {
        // make sure we are in edit mode
        if (Mode == DataBoundControlMode.Edit)
        {
            // try to get an item in the list that matched the FieldValueString
            ListItem item = DropDownList1.Items.FindByValue(FieldValueString);
            if (item != null)
            {
                // if we get the value set the drop down list to FieldValueString
                DropDownList1.SelectedValue = FieldValueString;
            }
        }
    }
    // This is one of those needed things I think it allows
    // access to the actual control through Controls property
    public override Control DataControl
    {
        get
        {
            return DropDownList1;
        }
    }
}

Listing 7 – Finished Enumeration_Edit.ascx.cs

Now we need an Enumeration.ascx file for this FieldTemplate all we need is to create a copy of the Text.ascx FieldTemplate control.

<%@ Control
    Language="C#"
    CodeFile="Enumeration.ascx.cs"
    Inherits="EnumerationField" %>

<asp:Literal
    runat="server"
    ID="Literal1"
    Text="<%# FieldValueString %>" />

Listing 8 – Enumeration.ascx

using System.Web.UI;

public partial class EnumerationField : System.Web.DynamicData.FieldTemplateUserControl
{
    public override Control DataControl
    {
        get
        {
            return Literal1;
        }
    }
}
Listing 9 – Enumeration.ascx.cs

This blatant copy if the Text.ascx FieldTemplate is due to the fact that our enumeration types don’t have a direct conversion to text and so don’t automatically drop through to the Text.ascx FieldTemplate.

Now we can edit and view our data.

Note: And yes I know that there is an Enumeration control in the Dynamic Data Futures and it doesn’t need the UIHint, but I just wanted something that was not too trivial :D