Tuesday, October 17, 2006

Extending FormView with Enhanced Design Time Support

Over the last week I started new ASP.NET project. After reviewing the requirements, I decided it was time to try out the new ASP.NET 2.0 FormView, GridView and DataSource controls. I had used these controls basic capabilities many times before, but this project was going to require more sophisticated use than any of my previous projects.

This series of articles will be focused on the use, limitations and enhancement of the built in FormView class and the FormViewDesigner. Also of note are some minor bugs discovered in the standard implementation of FormViewDesigner.

So it was time to start build a UI to View, Insert, Update and Delete data from a complex database. I had implemented my data access layer using TypedDataSets. The ObjectDataSource could connect easily to these. I built a few GridViews. Finally it was time to build the Viewers and Editors for the data.

I added my first FormView to a new ASP.NET page. Hooked into an ObjectDataSource and bag, I had templates generated for Insert, Update and View. I went in and started fixing the layout and adding in all my custom controls and validators. I hit F5 and there was my editor up and running in 30 minutes.

Then I realized, I had 30+ more Entities in the database that needed these editor build. At least 80% of what I did to the template code was just adding table layout and div/span and CSS class names. 30 tables x 30 minutes per table. It starts to add up for what amounts to code the Designer should be able to build.

Now I know why .NET can be so disappointing out of the box to Web Designers. In our case FormView’s build-in designer is very weak on the template code generation phase. You get a layout that is naive. Something like:

Label: <asp:label id="" runat="" text='<%#Bind(">???")'></asp:Label>

or


Label: <asp:textbox id="" runat="" text='<%#Bind(">???")'></asp:TextBox>


I plan on presenting a 4 part series of articles describing my solution to this problem. Stay tuned.

Step 1: Analyzing the Existing FormViewDesigner

Step 1: Analyzing the Existing FormViewDesigner.

The problem lay not in the FormView class, but in the standard FormViewDesigner class that is used by Visual Studio to implement the design time behavior.

I decided it was time to take a look under the hood of FormViewDesigner. For those of you that do not know about Lutz Roeder's .NET Reflector, check it out. You can use Reflector to decompile .NET assemblies into high-level code. Using the tool, you can pull apart the System.Web.UI.Design.WebControls.FormViewDesigner in the System.Design.dll assembly.

Several key methods pop out immediately with regards to the design time behavior:

protected override void OnSchemaRefreshed() {...}

private bool SchemaRefreshedCallback(object context) {...}

private void AddTemplatesAndKeys(IDataSourceViewSchema schema){...}


The first function is an override of the BaseDataBoundControlDesigner.OnSchemRefreshed() method. This method is called by the designer to notify subclasses when the DataSourceControl bound to the control is changed in some significant way.

The implementation in FormViewDesigner is:

protected override void OnSchemaRefreshed()
{
if (!base.InTemplateMode)
{
ControlDesigner.InvokeTransactedChange(
base.Component,
new TransactedChangeCallback(
this.SchemaRefreshedCallback),
null,
SR.GetString(
"DataControls_SchemaRefreshedTransaction"));
}
}


This method registers the TransactedChangeCallback delegate, a private method, SchemaRefreshedCallback(). [Though I’m not sure of the reason this callback mechanism is used, I suspect its one of those messy issues of UI thread synchronization.]

At some point the SchemaRefreshedCallback method is invoked assumedly on the correct thread.

private bool SchemaRefreshedCallback(object context)
{
IDataSourceViewSchema schema1 = this.GetDataSourceSchema();
if ((base.DataSourceID.Length > 0) && (schema1 != null))
{
if (((((FormView)base.Component).DataKeyNames.Length > 0)
(((FormView)base.Component).ItemTemplate != null))
(((FormView)base.Component).EditItemTemplate != null))
{
if (DialogResult.Yes == UIServiceHelper.ShowMessage(
base.Component.Site,
SR.GetString("FormView_SchemaRefreshedWarning"),
SR.GetString("FormView_SchemaRefreshedCaption",
new object[] { ((FormView)base.Component).ID }),
MessageBoxButtons.YesNo))
{
((FormView)base.Component).DataKeyNames =
new string[0];
this.AddTemplatesAndKeys(schema1);
}
}
else
{
this.AddTemplatesAndKeys(schema1);
}
}
else if ((((((FormView)base.Component).DataKeyNames.Length > 0)
(((FormView)base.Component).ItemTemplate != null))
(((FormView)base.Component).EditItemTemplate != null))
&& (DialogResult.Yes == UIServiceHelper.ShowMessage(
base.Component.Site,
SR.GetString(
"FormView_SchemaRefreshedWarningNoDataSource"),
SR.GetString("FormView_SchemaRefreshedCaption",
new object[] { ((FormView)base.Component).ID }),
MessageBoxButtons.YesNo)))
{
((FormView)base.Component).DataKeyNames =
new string[0];
((FormView)base.Component).ItemTemplate = null;
((FormView)base.Component).InsertItemTemplate = null;
((FormView)base.Component).EditItemTemplate = null;
}

this.UpdateDesignTimeHtml();
return true;
}

This method retrieves the schema of the DataSource and then does one of two things:

  1. Assuming the DataSourceID is defined and the DataSource returns a non-null IDataSourceSchema, the AddTemplatesAndKeys() method is called, causing new templates as well as the keys string to be updated. Note that if the Keys or Templates are already defined, the user is prompted to verify if they want to rebuild them.

  2. The alternative is that either the DataSourceID is empty or the IDataSourceScheme is null. This case occurs if the DataSourceID is cleared or some other issue exists where the schema cannot be retrieved. In this case, the DataKeyNames and the Insert, Update and Item templates are cleared.

Note: I found a serious problem in this code, even Microsoft guru’s are not infallible it appears. If you notice,

(((FormView) base.Component).InsertItemTemplate != null)

tests are missing from both the tests that effect template generation and removal. We will be fixing this in our implementation.

Case 1 is obviously the more interesting case for us, as we want to change the behavior of the template construction. It call another private method: AddTemplatesAndKeys(). So lets take a look at this method.

private void AddTemplatesAndKeys(IDataSourceViewSchema schema)
{
StringBuilder builder1 = new StringBuilder();
StringBuilder builder2 = new StringBuilder();
StringBuilder builder3 = new StringBuilder();
IDesignerHost host1 =
(IDesignerHost)base.Component.Site.GetService(
typeof(IDesignerHost));
if (schema != null)
{
IDataSourceFieldSchema[] schemaArray1 =
schema.GetFields();

if ((schemaArray1 != null) && (schemaArray1.Length > 0))
{
ArrayList list1 = new ArrayList();
foreach (IDataSourceFieldSchema schema1 in schemaArray1)
{
string text1 = schema1.Name;
char[] chArray1 = new char[text1.Length];
for (int num1 = 0; num1 < text1.Length; num1++)
{
char ch1 = text1[num1];
if (char.IsLetterOrDigit(ch1) (ch1 == '_'))
{
chArray1[num1] = ch1;
}
else
{
chArray1[num1] = '_';
}
}
string text2 = new string(chArray1);
string text3 =
DesignTimeDataBinding.CreateEvalExpression(
text1, string.Empty);
string text4 =
DesignTimeDataBinding.CreateBindExpression(
text1, string.Empty);
if (schema1.PrimaryKey schema1.Identity)
{
builder1.Append(
string.Format(CultureInfo.InvariantCulture,
"{0}: <asp:Label Text='<%# {1} %>' " + "runat=\"server\" id=\"{2}Label1\" /><br />",
new object[] { text1, text3, text2 }));
builder2.Append(
string.Format(CultureInfo.InvariantCulture,
"{0}: <asp:Label Text='<%# {1} %>' " + "runat=\"server\" id=\"{2}Label\" /><br />",
new object[] { text1, text3, text2 }));
if (!schema1.Identity)
{
builder3.Append(
string.Format(CultureInfo.InvariantCulture,
"{0}: <asp:TextBox Text='<%# {1} %>' " + "runat=\"server\" id=\"{2}TextBox\" />" +
"<br />",
new object[] { text1, text4, text2 }));
}
}
else if (schema1.DataType == typeof(bool))
{
builder1.Append(
string.Format(CultureInfo.InvariantCulture,
"{0}: <asp:CheckBox Checked='<%# {1} %>' " + "runat=\"server\" id=\"{2}CheckBox\" /><br />",
new object[] { text1, text4, text2 }));
builder2.Append(
string.Format(CultureInfo.InvariantCulture,
"{0}: <asp:CheckBox Checked='<%# {1} %>' " + "runat=\"server\" id=\"{2}CheckBox\" " + "Enabled=\"false\" /><br />",
new object[] { text1, text4, text2 }));
builder3.Append(
string.Format(CultureInfo.InvariantCulture,
"{0}: <asp:CheckBox Checked='<%# {1} %>' " + "runat=\"server\" id=\"{2}CheckBox\" /><br />",
new object[] { text1, text4, text2 }));
}
else
{
builder1.Append(
string.Format(CultureInfo.InvariantCulture,
"{0}: <asp:TextBox Text='<%# {1} %>' " + "runat=\"server\" id=\"{2}TextBox\" /><br />",
new object[] { text1, text4, text2 }));
builder2.Append(
string.Format(CultureInfo.InvariantCulture,
"{0}: <asp:Label Text='<%# {1} %>' " + "runat=\"server\" id=\"{2}Label\" /><br />",
new object[] { text1, text4, text2 }));
builder3.Append(
string.Format(CultureInfo.InvariantCulture,
"{0}: <asp:TextBox Text='<%# {1} %>' " + "runat=\"server\" id=\"{2}TextBox\" /><br />",
new object[] { text1, text4, text2 }));
}
builder1.Append(Environment.NewLine);
builder2.Append(Environment.NewLine);
builder3.Append(Environment.NewLine);
if (schema1.PrimaryKey)
{
list1.Add(text1);
}
}
bool flag1 = true;
if (base.DesignerView.CanUpdate)
{
builder2.Append(
string.Format(CultureInfo.InvariantCulture,
"<asp:LinkButton runat=\"server\" Text=\"{3}\" " + "CommandName=\"{0}\" id=\"{1}{0}Button\" " + "CausesValidation=\"{2}\" />",
new object[] { "Edit", string.Empty,
bool.FalseString,
SR.GetString("FormView_Edit") }));
flag1 = false;
}
builder1.Append(
string.Format(CultureInfo.InvariantCulture,
"<asp:LinkButton runat=\"server\" Text=\"{3}\" " + "CommandName=\"{0}\" id=\"{1}{0}Button\" " + "CausesValidation=\"{2}\" />",
new object[] { "Update", string.Empty,
bool.TrueString,
SR.GetString("FormView_Update") }));
builder1.Append(" ");
builder1.Append(
string.Format(CultureInfo.InvariantCulture,
"<asp:LinkButton runat=\"server\" Text=\"{3}\" " + "CommandName=\"{0}\" id=\"{1}{0}Button\" " + "CausesValidation=\"{2}\" />",
new object[] { "Cancel",
"Update",
bool.FalseString, SR.GetString("FormView_Cancel") }));
if (base.DesignerView.CanDelete)
{
if (!flag1)
{
builder2.Append(" ");
}

builder2.Append(
string.Format(CultureInfo.InvariantCulture,
"<asp:LinkButton runat=\"server\" " + "Text=\"{3}\" CommandName=\"{0}\" " + "id=\"{1}{0}Button\" " + "CausesValidation=\"{2}\" />",
new object[] { "Delete", string.Empty,
bool.FalseString,
SR.GetString("FormView_Delete") }));
flag1 = false;
}
if (base.DesignerView.CanInsert)
{
if (!flag1)
{
builder2.Append(" ");
}
builder2.Append(
string.Format(CultureInfo.InvariantCulture,
"<asp:LinkButton runat=\"server\" " + "Text=\"{3}\" CommandName=\"{0}\" " + "id=\"{1}{0}Button\" " + "CausesValidation=\"{2}\" />",
new object[] { "New", string.Empty,
bool.FalseString, SR.GetString("FormView_New") }));
}
builder3.Append(
string.Format(CultureInfo.InvariantCulture,
"<asp:LinkButton runat=\"server\" Text=\"{3}\"" +
" CommandName=\"{0}\" id=\"{1}{0}Button\" " + "CausesValidation=\"{2}\" />",
new object[] { "Insert", string.Empty,
bool.TrueString,
SR.GetString("FormView_Insert") }));
builder3.Append(" ");
builder3.Append(
string.Format(CultureInfo.InvariantCulture,
"<asp:LinkButton runat=\"server\" " +
"Text=\"{3}\" CommandName=\"{0}\" " + "id=\"{1}{0}Button\" CausesValidation=\"{2}\" />",
new object[] { "Cancel", "Insert",
bool.FalseString,
SR.GetString("FormView_Cancel") }));
builder1.Append(Environment.NewLine);
builder2.Append(Environment.NewLine);
builder3.Append(Environment.NewLine);
try
{
((FormView)base.Component).EditItemTemplate =
ControlParser.ParseTemplate(host1,
builder1.ToString());
((FormView)base.Component).ItemTemplate =
ControlParser.ParseTemplate(host1,
builder2.ToString());
((FormView)base.Component).InsertItemTemplate =
ControlParser.ParseTemplate(host1,
builder3.ToString());
}
catch
{
}
int num2 = list1.Count;
if (num2 > 0)
{
string[] textArray1 = new string[num2];
list1.CopyTo(textArray1, 0);
((FormView)base.Component).DataKeyNames = textArray1;
}
}
}
}

Wow, that is a mouth full. Let me try some pseudo-code to make it clear what is going on with this method:

private void AddTemplatesAndKeys(IDataSourceViewSchema schema)
{
// Define some builders to hold HTML code
// for our View, Edit and Update templates.
StringBuilder builder1 = new StringBuilder();
StringBuilder builder2 = new StringBuilder();
StringBuilder builder3 = new StringBuilder();

// Retrieve the designer host interface
IDesignerHost host1 = (IDesignerHost)
base.Component.Site.GetService(typeof(IDesignerHost));

// Assuming we have a schema
if (schema != null)
{
// Get the fields in the schema, this
// is the names of the columns from the DataSource.
IDataSourceFieldSchema[] schemaArray1 = schema.GetFields();

// Assuming we find some fields,
if ((schemaArray1 != null) && (schemaArray1.Length > 0))
{
// Process each field
foreach (IDataSourceFieldSchema schema1
in schemaArray1)
{

}
}

// Build the control buttons
// for the bottom of each template


// Construct the templates from the HTML text
try
{
((FormView) base.Component).EditItemTemplate =
ControlParser.ParseTemplate(host1, builder1.ToString());
((FormView) base.Component).ItemTemplate =
ControlParser.ParseTemplate(host1, builder2.ToString());
((FormView) base.Component).InsertItemTemplate =
ControlParser.ParseTemplate(host1, builder3.ToString());
}
catch { // Ignore exceptions }

// Construct and set the DataKeyNames field.

}
}


We retrieve the Fields from the Schema, iterate over them and build some HTML based on the Name, PrimaryKey, Identity, and DataType. The method distinguishes the follow characteristics of the field:

  • Is the field a primary key or an identity field? If so, it ads labels to the Edit and View templates and if its not an Identity field, it adds a text box to the Insert template.

  • Is the fields type Boolean? If so, it adds CheckBox controls to the template HTML.

  • If neither of these conditions is met, then it adds TextBox controls to the Insert and Update templates and a Label control to the View template.

The next step performed is to add control buttons to the HTML. I’ll leave that part to you to decipher, as it is not relevant to the changes I have in mind for my new Designer.

Finally the HTML held in the three StringBuilder objects is converted into ITemplate implementations using the ControlParsert.ParseTemplate() methods. Now you see where we needed the IDesignerHost interface we retrieved earlier.

Well after reviewing this class, I came to the realization that the .NET team didn’t really plan for this class to be enhanced to generate better template implementations. The use of all these private implementations signals that we as developers are probably not going to be enhancing this class through any easy overrides/inheritance techniques; at least not the kind of enhancements I have in mind. The .NET developers just didn’t leave us the hooks we needed to access the HTML construction engine.