Sunday 13 July 2008

Part 4 – A DetailsView and a GridView with seperate Validation *** UPDATED ***

As far as I can see there are three (oops! four then) 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

This page is a standard Edit.aspx PageTamplate with a few extra controls (GridView, LinqDataSource, DynamicValidator and ValidationSummary)

<asp:ValidationSummary 
    runat="server" 
    ID="OrdersValidationSummary" 
    EnableClientScript="true"
    ValidationGroup="Orders" 
    HeaderText="List of validation errors for Orders" />
    
<asp:DynamicValidator 
    runat="server" 
    ID="OrdersValidator" 
    ControlToValidate="dvOrders"
    ValidationGroup="Orders" 
    Display="None" />
    
<asp:DetailsView 
    runat="server" 
    ID="dvOrders" 
    DataSourceID="ldsOrders" 
    DefaultMode="Edit"
    OnItemCommand="dvOrders_ItemCommand" 
    OnItemUpdated="dvOrders_ItemUpdated" 
    CssClass="detailstable"
    FieldHeaderStyle-CssClass="bold" 
    AutoGenerateRows="True" 
    DataKeyNames="OrderID">
    <FieldHeaderStyle CssClass="bold" />
    <Fields>
        <asp:TemplateField ShowHeader="False">
            <EditItemTemplate>
                <asp:LinkButton 
                    ID="LinkButton1" 
                    runat="server" 
                    CausesValidation="True" 
                    ValidationGroup="Orders"
                    CommandName="Update" 
                    Text="Update">
                </asp:LinkButton>
                &nbsp;
                <asp:LinkButton 
                    ID="LinkButton2" 
                    runat="server" 
                    CausesValidation="False" 
                    CommandName="Cancel"
                    Text="Cancel">
                </asp:LinkButton>
            </EditItemTemplate>
        </asp:TemplateField>
    </Fields>
</asp:DetailsView>

<asp:LinqDataSource 
    runat="server" 
    ID="ldsOrders" 
    EnableUpdate="True" 
    ContextTypeName="NorthwindDataContext"
    TableName="Orders">
    <WhereParameters>
        <asp:Parameter />
    </WhereParameters>
</asp:LinqDataSource>

<br />

<asp:ValidationSummary 
    runat="server" 
    ID="Order_DetailsSummary" 
    EnableClientScript="true"
    ValidationGroup="Order_Details" 
    HeaderText="List of validation errors for Order_Details" />
    
<asp:DynamicValidator 
    runat="server" 
    ID="dyvOrder_Details" 
    ControlToValidate="gvOrder_Details"
    ValidationGroup="Order_Details" 
    Display="None" />
    
<asp:GridView 
    ID="gvOrder_Details" 
    runat="server" 
    DataKeyNames="OrderID,ProductID"
    CssClass="gridview"
    DataSourceID="ldsOrder_Details">
    <Columns>
        <asp:TemplateField ShowHeader="False">
            <EditItemTemplate>
                <asp:LinkButton 
                    ID="LinkButton1" 
                    runat="server" 
                    CausesValidation="True" 
                    ValidationGroup="Order_Details"
                    CommandName="Update" 
                    Text="Update">
                </asp:LinkButton>
                &nbsp;
                <asp:LinkButton 
                    ID="LinkButton2" 
                    runat="server" 
                    CausesValidation="False" 
                    CommandName="Cancel"
                    Text="Cancel">
                </asp:LinkButton>
            </EditItemTemplate>
            <ItemTemplate>
                <asp:LinkButton 
                    ID="LinkButton1" 
                    runat="server" 
                    CausesValidation="False" 
                    CommandName="Edit"
                    Text="Edit">
                </asp:LinkButton>
            </ItemTemplate>
        </asp:TemplateField>
    </Columns>
</asp:GridView>

<asp:LinqDataSource 
    ID="ldsOrder_Details" 
    runat="server" 
    ContextTypeName="NorthwindDataContext"
    TableName="Order_Details" 
    Where="OrderID == @OrderID" 
    EnableUpdate="True">
    <WhereParameters>
        <asp:ControlParameter 
            ControlID="dvOrders" 
            Name="OrderID" 
            PropertyName="SelectedValue"
            Type="Int32" />
    </WhereParameters>
</asp:LinqDataSource>

Listing 1 – Custom Edit page for Orders

In the above listing you can see that the validation controls and link buttons in the DetailsView and GridView have been decorated with ValidationGroup="Orders" and ValidationGroup="Order_Details" appropriately.

Note: You must set the ValidationGroup of the DetailsView/GridView data controls command buttons that update the data so that they are in the correct validation group otherwise the error will drop through the validation net and appear on the page as an un-handled exception.
Note: ValidationGroups allow you to assign validation controls on a page to a specific category. Each validation group can be validated independently of other validation groups on the page.

The problem you have with a DetailsView or GridView with AutoGenerateRows set to true is how do you set the item controls ValidationGroup.

public class ValidationGroupFieldsManager : IAutoFieldGenerator
{
    protected MetaTable _table;
    protected String _validationGroup;

    public ValidationGroupFieldsManager(MetaTable table, String validationGroup)
    {
        _table = table;
        _validationGroup = validationGroup;
    }

    public ICollection GenerateFields(Control control)
    {
        List<DynamicField> oFields = new List<DynamicField>();

        foreach (MetaColumn column in _table.Columns)
        {

            // carry on the loop at the next column  
            // if scaffold table is set to false or DenyRead
            if (!column.Scaffold)
                continue;

            // create a new field with its validation group set
            DynamicField f = new ValidationGroupDynamicField(_validationGroup);

            f.DataField = column.Name;
            oFields.Add(f);
        }
        return oFields;
    }
}

// special thanks to David Ebbo for this
public class ValidationGroupDynamicField : DynamicField
{
    protected String _validationGroup;

    public ValidationGroupDynamicField(String validationGroup)
    {
        _validationGroup = validationGroup;
    }

    public override void InitializeCell(
        DataControlFieldCell cell,
        DataControlCellType cellType,
        DataControlRowState rowState,
        int rowIndex)
    {
        if (cellType == DataControlCellType.DataCell)
        {
            var control = new DynamicControl()
                {
                    DataField = DataField,
                    // Copy various properties into the control
                    UIHint = UIHint,
                    HtmlEncode = HtmlEncode,
                    NullDisplayText = NullDisplayText,
                    // set ValidationGroup
                    ValidationGroup = _validationGroup
                };

            if ((rowState & DataControlRowState.Edit) != 0)
                control.Mode = DataBoundControlMode.Edit;

            if ((rowState & DataControlRowState.Insert) != 0)
                control.Mode = DataBoundControlMode.Insert;

            cell.Controls.Add(control);
        }
        else
        {
            base.InitializeCell(cell, cellType, rowState, rowIndex);
        }
    }
}

Listing 2 – IAutoFieldGenrator for setting the Mode and ValidationGroup

There are two classes in Listing 2 the first an example of an implementation of IAutoFieldGenrator. In this implementation all we do is create a DynamicField if there should be a column or row.

The clever bit (smile_embaressed if you can call it that) is in the my DynamicField implementation. In here we create a DynamicField and add a DynamicControl to it, set various properties including ValidationGroup (which was passed in via the constructor), then we set the Mode of the control by testing the rowState.

public partial class Edit : System.Web.UI.Page
{
    protected MetaTable mtOrders;
    protected MetaTable mtOrder_Details;

    protected void Page_Init(object sender, EventArgs e)
    {
        DynamicDataManager1.RegisterControl(dvOrders);

DynamicDataManager1.RegisterControl(gvOrder_Details);





// the following four lines were moved form Page_Load (cut and paste error on my part) mtOrders = ldsOrders.GetTable(); mtOrder_Details = ldsOrder_Details.GetTable(); dvOrders.RowsGenerator = new ValidationGroupFieldsManager(mtOrders, "Orders"); gvOrder_Details.ColumnsGenerator = new ValidationGroupFieldsManager(mtOrder_Details, "Order_Details"); } protected void Page_Load(object sender, EventArgs e) { mtOrders = ldsOrders.GetTable(); Title = mtOrders.DisplayName; } protected void dvOrders_ItemCommand(object sender, DetailsViewCommandEventArgs e) { if (e.CommandName == DataControlCommands.CancelCommandName) { Response.Redirect(mtOrders.ListActionPath); } } protected void dvOrders_ItemUpdated(object sender, DetailsViewUpdatedEventArgs e) { if (e.Exception == null || e.ExceptionHandled) { Response.Redirect(mtOrders.ListActionPath); } } }

Listing 3 – Edit.apsx.cs code behind file

In here we add the plumbing for the extra GridView and then add the RowGenrator and ColumnGenrator to the DetailsView and GridView to set the ValidationGroup for the individual fields in the both the DetailsView and GridView.

This give us the ability to have multiple DynamicData Lists, DetailsViews etc on a page and validate each one separately, which again put the validation where is should be in a central place OAOO (Once And Once Only) that I like so much smile_teeth.

Note: There appears to be a problem with the above at the moment see this thread Setting ValidationGroup on DynamicValidator and DynamicControl has... and also my post to this thread Re: How to submit and validate a dynamic data page programatically? I’m pretty sure that there will be a resolution to the problem soonsmile_teeth This was erroneous I was feeling a bit DUMB smile_embaressed but I’m better now. The issue I’m trying to fix is where you have customised the page’s columns using DynamicField which you cannot set the ValidationGroup declaratively, I’m trying to do this at runtime on the OnDataBound event. I’ll post an update when I’ve figured it out.
It would seem my Dumbness continued! It’s too late in the page creation cycle to try and set the ValidationGroup, this is because Dynamic Data uses FieldTemplates which are actually User Controls which means that they are made up of several controls. So you would have to drill down into the DynamicControl and set the ValidationGroup of each of the separate controls that make up the FieldTemplate user control. Yes you could do but it’s a lot of messing about better to just use DynamicControl over DynamicField in those situations even if the markup is more fiddly. smile_teeth

No comments: