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.

1 comment:

J said...

Steve. good stuff, and I appreciate your responses on the asp.net forum. I'm wondering if you can point me in a good direction.

I'm having a hard time getting the ListView to do inserts using EntityDataSource Dynamic Data. I have been unable to find any documentation, and I've been struggling with this for days. Any Ideas. I posted this at the forum here http://forums.asp.net/p/1311101/2581836.aspx#2581836

Many Thanks,
Jeff