Tuesday, 23 September 2008

Dynamic Data and Advanced Field Templates - ParentFormView with Templates

ParentFormView with Templates

This is a variant on the DetailsView/ParentDetails FieldTemplate the main difference is that here the FieldTemplate is based on a FormView not a DetailsView.

<asp:FormView 
    ID="FormView1" 
    runat="server" 
    DataSourceID="FormDataSource"
    CssClass="detailstable"
    FieldHeaderStyle-CssClass="bold">
    
</asp:FormView>

<asp:LinqDataSource 
    ID="FormDataSource" 
    runat="server" 
    EnableDelete="true">
</asp:LinqDataSource>

Listing 1 – changes to the FieldTemplate

In Listing 1 the DetilsView has been changed to a FormView. All references to DetailsView1 and DetailsDataSource have been changed to FormView1 and FormDataSource in the ascx page and the code behind.

if (metaForeignKeyColumn != null)
{
    // load Field Template
    String ddFolderVirtualPath = metaForeignKeyColumn.ParentTable.Model.DynamicDataFolderVirtualPath;
    String tableName = metaForeignKeyColumn.ParentTable.Name;

    String itemTemplate = ddFolderVirtualPath + "Templates/" + tableName + "Item.ascx";
    String editItemTemplate = ddFolderVirtualPath + "Templates/" + tableName + "Edit.ascx";
    //String insertItemTemplate = ddFolderVirtualPath + "Templates/" + tableName + "Insert.ascx";
    String footerTemplate = ddFolderVirtualPath + "Templates/" + tableName + "Footer.ascx";
    String headerTemplate = ddFolderVirtualPath + "Templates/" + tableName + "Header.ascx";
    String pagerTemplate = ddFolderVirtualPath + "Templates/" + tableName + "Pager.ascx";

    if (File.Exists(Server.MapPath(itemTemplate)))
    {
        FormView1.ItemTemplate = LoadTemplate(itemTemplate);

        // load any other field templates
        //TODO: if an operation is required but no template throw error
        if (File.Exists(Server.MapPath(editItemTemplate)))
            FormView1.EditItemTemplate = LoadTemplate(editItemTemplate);

        // because this is a parent no insert will be available
        //if (File.Exists(Server.MapPath(insertItemTemplate))) 
        //    FormView1.InsertItemTemplate = LoadTemplate(insertItemTemplate);

        if (File.Exists(Server.MapPath(footerTemplate)))
            FormView1.FooterTemplate = LoadTemplate(footerTemplate);
        if (File.Exists(Server.MapPath(headerTemplate)))
            FormView1.HeaderTemplate = LoadTemplate(headerTemplate);
        if (File.Exists(Server.MapPath(pagerTemplate)))
            FormView1.PagerTemplate = LoadTemplate(pagerTemplate);

        ...//CODE OMITTED FOR CLARITY
    }
    else
    {
        throw new InvalidOperationException("ParentFormView_Edit requires an item template.");
    }
}
else
{
    // throw an error if set on column other than MetaChildrenColumns
    throw new InvalidOperationException("The GridView FieldTemplate can only be used with MetaChildrenColumns");
}

Listing 2 – changes to the code behind

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

    // get the FK column
    var metaForeignKeyColumn = Column as MetaForeignKeyColumn;

    // get the association attributes associated with MetaChildrenColumns
    var association = metaForeignKeyColumn.Attributes.
        OfType<System.Data.Linq.Mapping.AssociationAttribute>().FirstOrDefault();

    if (metaForeignKeyColumn != null && association != null)
    {
        // get keys ThisKey and OtherKey into dictionary
        var keys = new Dictionary<String, String>();
        var seperator = new char[] { ',' };
        var thisKeys = association.ThisKey.Split(seperator);
        var otherKeys = association.OtherKey.Split(seperator);
        for (int i = 0; i < thisKeys.Length; i++)
        {
            keys.Add(thisKeys[i], otherKeys[i]);
        }

        // setup the where clause 
        // support composite foreign keys
        foreach (String fkName in metaForeignKeyColumn.ForeignKeyNames)
        {
            // get the current FK column
            var fkColumn = metaForeignKeyColumn.Table.GetColumn(fkName);
            // get the current PK column
            var pkColumn = metaForeignKeyColumn.ParentTable.GetColumn(keys[fkName]);

            // setup parameter
            var param = new Parameter();
            param.Name = pkColumn.Name;
            param.Type = pkColumn.TypeCode;

            // get the value for this FK column
            param.DefaultValue = GetColumnValue(fkColumn).ToString();

            // add the where clause
            FormDataSource.WhereParameters.Add(param);
        }
    }
}

Listing 3 – OnDataBinding now handles multiple PK-FK relationships ***UPDATED 2008/09/24***

As you can see what has been done here add the facility to load templates for the FormView to allow this I’ve added a new folder to the DynamicData folder named Templates see Figure 1.

Folder structure changes

Figure 1 - Folder structure changes

In Listing 2 you can see that Model.DynamicDataFolderVirtualPath is accessed to retrieve the Dynamic Data virtual path (the default is ~/DynamicData/) so that the path to each Template can be constructed. The path to each of the possible Templates is constructed, but only the itemTemplate is required.

Note: Code to test for other Templates could be added if that option was enabled via properties set via the constructor.

All you need to create a Template file is to add a new UserControl to the Templates folder and then customise it.

<p class="droplist">
<strong>Name: </strong><asp:DynamicControl ID="DynamicControl1" runat="server" DataField="CompanyName" ></asp:DynamicControl>&nbsp;
<strong>Contact Name: </strong><asp:DynamicControl ID="DynamicControl2" runat="server" DataField="ContactName" ></asp:DynamicControl>
</p>
<p class="droplist">
<strong>Phone: </strong><asp:DynamicControl ID="DynamicControl3" runat="server" DataField="Phone" ></asp:DynamicControl>&nbsp;
<strong>Fax: </strong><asp:DynamicControl ID="DynamicControl4" runat="server" DataField="Fax" ></asp:DynamicControl>
</p>

Listing 4 – a very simple ItemTemplate

As you can see Listing 4 is a very simple field template but looks how you would expect the mark-up to look in Figure 2.

ScreenShot119

Figure 2 – the FieldTemplate at work

Project Download

6 comments:

Erkka said...

Inserting with DetailsView is something I was too eager to wait for so here's how I did it :)

DataSource:

asp:LinqDataSource
ID="DetailsDataSource"
runat="server"
EnableDelete="true" OnInserting="DetailsDataSource_Inserting"
asp:LinqDataSource

Code Behind:

public partial class ParentDetails_EditField : FieldTemplateUserControl
{
protected MetaTable parentTable;
protected MetaTable childTable;
public Object newObject;
public Boolean EnableDelete { get; set; }
public Boolean EnableInsert { get; set; }
public Boolean EnableUpdate { get; set; }

public String[] DisplayColumns { get; set; }

public ParentDetails_EditField()
{
// set default values
EnableDelete = true;
EnableUpdate = true;
EnableInsert = true;
}

protected void Page_Init(object sender, EventArgs e)
{
var attribute = Column.Attributes.OfType'<'ShowColumnsAttribute'>'().SingleOrDefault();

if (attribute != null)
{
if (!attribute.EnableDelete)
EnableDelete = false;
if (!attribute.EnableUpdate)
EnableUpdate = false;
if (!attribute.EnableInsert)
EnableInsert = false;
if (attribute.DisplayColumns.Length > 0)
DisplayColumns = attribute.DisplayColumns;
}

var metaForeignKeyColumn = Column as MetaForeignKeyColumn;

if (metaForeignKeyColumn != null)
{
childTable = metaForeignKeyColumn.Table;

// setup data source
DetailsDataSource.ContextTypeName = metaForeignKeyColumn.ParentTable.DataContextType.Name;
DetailsDataSource.TableName = metaForeignKeyColumn.ParentTable.Name;

// enable update, delete and insert
DetailsDataSource.EnableDelete = EnableDelete;
DetailsDataSource.EnableInsert = EnableInsert;
DetailsDataSource.EnableUpdate = EnableUpdate;
DetailsView1.AutoGenerateDeleteButton = EnableDelete;
// We don't need InsertButton
DetailsView1.AutoGenerateInsertButton = false; // EnableInsert;
DetailsView1.AutoGenerateEditButton = EnableUpdate;

// get an instance of the MetaTable
parentTable = DetailsDataSource.GetTable();

// Generate the columns as we can't rely on
// DynamicDataManager to do it for us.
DetailsView1.RowsGenerator = new FieldTemplateRowGenerator(parentTable, DisplayColumns);

// setup the GridView's DataKeys
String[] keys = new String[metaForeignKeyColumn.ParentTable.PrimaryKeyColumns.Count];
int i = 0;
foreach (var keyColumn in metaForeignKeyColumn.ParentTable.PrimaryKeyColumns)
{
keys[i] = keyColumn.Name;
i++;
}
DetailsView1.DataKeyNames = keys;

// enable AutoGenerateWhereClause so that the WHERE
// clause is generated from the parameters collection
DetailsDataSource.AutoGenerateWhereClause = true;

// doing the work of this above because we can't
// set the DynamicDataManager table or where values
//DynamicDataManager1.RegisterControl(DetailsView1, false);
}
else
{
// throw an error if set on column other than MetaChildrenColumns
throw new InvalidOperationException("The GridView FieldTemplate can only be used with MetaChildrenColumns");
}
}

protected override void OnDataBinding(EventArgs e)
{
var metaForeignKeyColumn = Column as MetaForeignKeyColumn;

if (metaForeignKeyColumn != null)
{

// setup the where clause
// support composite foreign keys
foreach (String fkName in metaForeignKeyColumn.ForeignKeyNames)
{
// get the current fk column
var fkColumn = childTable.GetColumn(fkName);

// setup parameter
var param = new Parameter();
param.Name = fkColumn.Name;
param.Type = fkColumn.TypeCode;
if (GetColumnValue(fkColumn) != null)
{
// get the value for this FK column
param.DefaultValue = GetColumnValue(fkColumn).ToString();

// add the where clause
DetailsDataSource.WhereParameters.Add(param);
}
else
{
if (EnableInsert == true)
{ DetailsView1.ChangeMode(DetailsViewMode.Insert); }
else
{ DetailsView1.Visible = false; }
}
}
}

base.OnDataBinding(e);
}
public void DetailsDataSource_Inserting(object sender, LinqDataSourceInsertEventArgs e)
{
newObject = e.NewObject;
e.Cancel = true;
}
protected override void ExtractValues(IOrderedDictionary dictionary)
{
if (IsPostBack && DetailsView1.CurrentMode == DetailsViewMode.Insert)
{
DetailsView1.InsertItem(true);
dictionary[Column.Name] = newObject;
}
}
}

Works for me. What do you think?

Steve said...

Hi Erkka, I still don't see (and this might be me being a bit dumb) why you would want to insert a new parent record?

Steve :D

Anonymous said...

i have download your demo code and find there is an err in insert with ParentDetails template
the message
System.NullReferenceException
param.DefaultValue = GetColumnValue(fkColumn).ToString();

Steve said...
This comment has been removed by the author.
Steve said...

Sorry about that, I've already said once before that that is not an option but I think it was in the Dynamic Data Forum So I'll post an update here also.

Please see the first comment here by Erkka and the sample code that may help.

Steve :D

Steve said...

Please see Update on this article here:
http://csharpbits.notaclue.net/2008/09/dynamic-data-and-field-templates_06.html