Pages

Sunday, May 29, 2005

How to write common code that generates Win Forms as well as Web Forms in .NET using XML

Introduction


This is a concept piece that is intended to whet your appetite for declarative programming and the idea that maybe, just maybe, it is possible to unify development for System.Windows.Forms and System.Web.UI.WebControls namespaces, at least for simple applications.


Now, granted, "real" web pages have lots of JavaScript to reduce post-backs, go through a lot of optimization at the server, need to deal with real world issues like traffic, etc. And a calculator example, where each button click is a post-back, is definitely not the best example!


Click here for an online demo.


How Does It Work?


Very simple. Given a MyXaml markup file that defines the UI in the System.Windows.Forms namespace, a very simple XSLT, written by Justin, converts the markup to one that can live in the System.Web.UI.WebControls namespace. So, one markup file drives both Form and Web UI's, and the event handler code is identical as well except for one line.


The Markup


The source markup is very simple. No styles, no sub-forms, etc. Simple is better when trying to pull this off.

<?xml version="1.0" encoding="utf-8"?>
<!-- (c) 2004 Marc Clifton All Rights Reserved -->
<wf:MyXaml
xmlns:wf="System.Windows.Forms"
xmlns:def="Definitions">
<wf:Panel Name="Calc" Location="10, 10" Size="600, 400">
<Controls>
<wf:TextBox def:Name="display" Location="0, 0" Size="155, 20"
BackColor="Black" ForeColor="Yellow" Text="0.00"/>

<wf:Button Text="C" Location="0, 20" Size="60, 25"
Click="OnClear" FlatStyle="System"/>
<wf:Button Text="CE" Location="60, 20" Size="30, 25"
Click="OnClearEntry" FlatStyle="System"/>
<wf:Button Text="=" Location="95, 20" Size="60, 25"
Click="OnEqual" FlatStyle="System"/>

<wf:Button Text="7" Location="0, 50" Size="30, 28"
Click="OnDigit" FlatStyle="System"/>
<wf:Button Text="8" Location="30, 50" Size="30, 28"
Click="OnDigit" FlatStyle="System"/>
<wf:Button Text="9" Location="60, 50" Size="30, 28"
Click="OnDigit" FlatStyle="System"/>
<wf:Button Text="4" Location="0, 80" Size="30, 28"
Click="OnDigit" FlatStyle="System"/>
<wf:Button Text="5" Location="30, 80" Size="30, 28"
Click="OnDigit" FlatStyle="System"/>
<wf:Button Text="6" Location="60, 80" Size="30, 28"
Click="OnDigit" FlatStyle="System"/>
<wf:Button Text="1" Location="0, 110" Size="30, 28"
Click="OnDigit" FlatStyle="System"/>
<wf:Button Text="2" Location="30, 110" Size="30, 28"
Click="OnDigit" FlatStyle="System"/>
<wf:Button Text="3" Location="60, 110" Size="30, 28"
Click="OnDigit" FlatStyle="System"/>
<wf:Button Text="0" Location="0, 140" Size="30, 28"
Click="OnDigit" FlatStyle="System"/>
<wf:Button Text="." Location="60, 140" Size="30, 28"
Click="OnDigit" FlatStyle="System"/>

<wf:Button Text="+" Location="95, 50" Size="60, 28"
Click="OnAdd" FlatStyle="System"/>
<wf:Button Text="-" Location="95, 80" Size="60, 28"
Click="OnSubtract" FlatStyle="System"/>
<wf:Button Text="*" Location="95, 110" Size="60, 28"
Click="OnMultiply" FlatStyle="System"/>
<wf:Button Text="/" Location="95, 140" Size="60, 28"
Click="OnDivide" FlatStyle="System"/>
</Controls>
</wf:Panel>
</wf:MyXaml>


What Gets Generated?


The XSLT translates this to:

<wf:MyXaml
xmlns:wf="System.Web.UI.WebControls, System.Web, Version=1.0.5000.0,
Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"
xmlns:def="Definitions">
<wf:Panel Name="Calc" CssStyle="position:absolute; left:10px;
top:10px; width:600px; height:400px;">
<Controls>
<wf:TextBox def:Name="display" BackColor="Black" ForeColor="Yellow"
Text="0.00" CssStyle="position:absolute; left:0px; top:0px;
width:155px; height:20px;"></wf:TextBox>

<wf:Button Text="C" Click="OnClear" FlatStyle="System"
CssStyle="position:absolute; left:0px; top:20px; width:60px;
height:25px;"></wf:Button>
<wf:Button Text="CE" Click="OnClearEntry" FlatStyle="System"
CssStyle="position:absolute; left:60px; top:20px; width:30px;
height:25px;"></wf:Button>
<wf:Button Text="=" Click="OnEqual" FlatStyle="System"
CssStyle="position:absolute; left:95px; top:20px; width:60px;
height:25px;"></wf:Button>

<wf:Button Text="7" Click="OnDigit" FlatStyle="System"
CssStyle="position:absolute; left:0px; top:50px; width:30px;
height:28px;"></wf:Button>
<wf:Button Text="8" Click="OnDigit" FlatStyle="System"
CssStyle="position:absolute; left:30px; top:50px; width:30px;
height:28px;"></wf:Button>
<wf:Button Text="9" Click="OnDigit" FlatStyle="System"
CssStyle="position:absolute; left:60px; top:50px; width:30px;
height:28px;"></wf:Button>
<wf:Button Text="4" Click="OnDigit" FlatStyle="System"
CssStyle="position:absolute; left:0px; top:80px; width:30px;
height:28px;"></wf:Button>
<wf:Button Text="5" Click="OnDigit" FlatStyle="System"
CssStyle="position:absolute; left:30px; top:80px; width:30px;
height:28px;"></wf:Button>
<wf:Button Text="6" Click="OnDigit" FlatStyle="System"
CssStyle="position:absolute; left:60px; top:80px; width:30px;
height:28px;"></wf:Button>
<wf:Button Text="1" Click="OnDigit" FlatStyle="System"
CssStyle="position:absolute; left:0px; top:110px; width:30px;
height:28px;"></wf:Button>
<wf:Button Text="2" Click="OnDigit" FlatStyle="System"
CssStyle="position:absolute; left:30px; top:110px; width:30px;
height:28px;"></wf:Button>
<wf:Button Text="3" Click="OnDigit" FlatStyle="System"
CssStyle="position:absolute; left:60px; top:110px; width:30px;
height:28px;"></wf:Button>
<wf:Button Text="0" Click="OnDigit" FlatStyle="System"
CssStyle="position:absolute; left:0px; top:140px; width:30px;
height:28px;"></wf:Button>
<wf:Button Text="." Click="OnDigit" FlatStyle="System"
CssStyle="position:absolute; left:60px; top:140px; width:30px;
height:28px;"></wf:Button>

<wf:Button Text="+" Click="OnAdd" FlatStyle="System"
CssStyle="position:absolute; left:95px; top:50px; width:60px;
height:28px;"></wf:Button>
<wf:Button Text="-" Click="OnSubtract" FlatStyle="System"
CssStyle="position:absolute; left:95px; top:80px; width:60px;
height:28px;"></wf:Button>
<wf:Button Text="*" Click="OnMultiply" FlatStyle="System"
CssStyle="position:absolute; left:95px; top:110px; width:60px;
height:28px;"></wf:Button>
<wf:Button Text="/" Click="OnDivide" FlatStyle="System"
CssStyle="position:absolute; left:95px; top:140px; width:60px;
height:28px;"></wf:Button>
</Controls>
</wf:Panel>
</wf:MyXaml>


What Does The XSLT Look Like?


The XSLT is straightforward enough:

<?xml version="1.0" encoding="UTF-8" ?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml"/>

<xsl:template match="*">
<xsl:choose>
<xsl:when test="namespace-uri(.) = 'System.Windows.Forms'">
<xsl:element name="{name(.)}" namespace="System.Web.UI.WebControls,
System.Web, Version=1.0.5000.0, Culture=neutral,
PublicKeyToken=b03f5f7f11d50a3a">
<xsl:copy-of select="namespace::*[not(.='System.Windows.Forms')]"/>
<xsl:copy-of select="@*[not(name()='Location') and not(name()='Size')]"/>
<xsl:if test="@Location or @Size">
<xsl:attribute name="CssStyle">
<xsl:if test="@Location">position:absolute; left:<xsl:value-of
select="normalize-space(substring-before(@Location,','))"/>px;
top:<xsl:value-of
select="normalize-space(substring-after(@Location,','))"/>px;
</xsl:if>
<xsl:if test="@Size">width:<xsl:value-of
select="normalize-space(substring-before(@Size,','))"/>px;
height:<xsl:value-of
select="normalize-space(substring-after(@Size,','))"/>px;
</xsl:if>
</xsl:attribute>
</xsl:if>
<xsl:apply-templates></xsl:apply-templates>
</xsl:element>
</xsl:when>
<xsl:otherwise>
<xsl:element name="{name(.)}" namespace="{namespace-uri(.)}">
<xsl:copy-of select="namespace::*[not(.='System.Windows.Forms')]"/>
<xsl:copy-of select="@*[not(name()='Location') and not(name()='Size')]"/>
<xsl:if test="@Location or @Size">
<xsl:attribute name="CssStyle">
<xsl:if test="@Location">position:absolute; left:<xsl:value-of
select="normalize-space(substring-before(@Location,','))"/>px;
top:<xsl:value-of
select="normalize-space(substring-after(@Location,','))"/>px;
</xsl:if>
<xsl:if test="@Size">width:<xsl:value-of
select="normalize-space(substring-before(@Size,','))"/>px;
height:<xsl:value-of
select="normalize-space(substring-after(@Size,','))"/>px;
</xsl:if>
</xsl:attribute>
</xsl:if>
<xsl:apply-templates></xsl:apply-templates>
</xsl:element>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>


The Loaders


To make all this magic work, the markup has to be parsed, which is done by MyXaml. The parsing is done in the application startup. For the Form version, this is done when Main() is called. In the Web version, it's done in Page_Load event handler.


The Form Loader


The Form version has a very simple loader:

using System;
using System.Drawing;
using System.Collections;
using System.ComponentModel;
using System.Windows.Forms;
using System.Data;

using MyXaml;

namespace declarativeForm
{
public class App
{
[STAThread]
static void Main()
{
new App();
}

public App()
{
Parser p=new Parser();
SimpleCalc sc=new SimpleCalc();
Panel panel=(Panel)p.LoadObject("calcForm.xml", "Calc", sc, null);
Form form=new Form();
form.Controls.Add(panel);
Application.Run(form);
}
}
}


The Web Loader


The Web loader is more complicated because there is no concept of session state. Therefore, the class that manages the state of the calculator (concatenating digits, remembering the last operator, etc.) has to be preserved in the Session container. Also, for every post-back, the UI has to be regenerated.

using System;
using System.Collections;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Text;
using System.Web;
using System.Web.SessionState;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.HtmlControls;
using System.Xml;
using System.Xml.Xsl;

using MyXaml;

namespace declarativeWeb
{
public class WebForm1 : System.Web.UI.Page
{
protected System.Web.UI.WebControls.Panel formPanel;

private void Page_Load(object sender, System.EventArgs e)
{
if (!IsPostBack)
{
SimpleCalc sc=new SimpleCalc();
XmlDocument doc=LoadDocument();

if (doc != null)
{
Parser parser=new Parser();
Panel panel=(Panel)parser.LoadObject(doc, "Calc", sc, null);
if (panel != null)
{
formPanel.Controls.Add(panel);
}
}

Session["SimpleCalc"]=sc;
}
else
{
SimpleCalc sc=(SimpleCalc)Session["SimpleCalc"];
XmlDocument doc=LoadDocument();

if (doc != null)
{
Parser parser=new Parser();
Panel panel=(Panel)parser.LoadObject(doc, "Calc", sc, null);
if (panel != null)
{
formPanel.Controls.Add(panel);
}
}
}
}

override protected void OnInit(EventArgs e)
{
InitializeComponent();
base.OnInit(e);
}

private void InitializeComponent()
{
this.Load += new System.EventHandler(this.Page_Load);
}

private XmlDocument LoadDocument()
{
XmlDocument doc=null;
string path=MapPath("");

try
{
doc=new XmlDataDocument();
doc.PreserveWhitespace=true;
doc.Load(path+"\\calcForm.xml");
XslTransform xt=new XslTransform();
xt.Load(path+"\\form2web.xslt");
StringBuilder sb=new StringBuilder();
StringWriter sw=new StringWriter(sb);
xt.Transform(doc, null, new XmlTextWriter(sw), new XmlUrlResolver());

doc=new XmlDocument();
doc.LoadXml(sb.ToString());
System.Diagnostics.Trace.WriteLine(sb.ToString());

}
catch(Exception ex)
{
Trace.Warn(ex.Message);
}
return doc;
}
}
}


The Calculator Event Handler Code


The calculator event handler code is identical for both Form and Web versions, except in the Form version you have to specify:

using System.Windows.Forms;


whereas in the Web version, you specify:

using System.Web.UI.WebControls;


Here is the Web version:

using System;
using System.Globalization;
using System.Web.UI.WebControls; // Form version is System.Windows.Forms

using MyXaml;

namespace declarativeWeb
{
public class SimpleCalc
{
private bool cleared;
private string lastOp;
private string lastValue;
private NumberFormatInfo formatProvider;

[MyXamlAutoInitialize] private TextBox display=null;

public SimpleCalc()
{
cleared=true;
lastOp=String.Empty;
lastValue=String.Empty;

formatProvider=new NumberFormatInfo();
formatProvider.NumberDecimalDigits=2;
}

public void OnClear(object sender, EventArgs e)
{
display.Text="0.00";
cleared=true;
lastOp=String.Empty;
lastValue=String.Empty;
}

public void OnClearEntry(object sender, EventArgs e)
{
display.Text="0.00";
cleared=true;
}

public void OnDigit(object sender, EventArgs e)
{
Button btn=(Button)sender;
if (cleared)
{
display.Text=btn.Text;
cleared=false;
}
else
{
display.Text=display.Text+btn.Text;
}
}

public string ProcessLastOp(string val)
{
double d=0;
switch(lastOp)
{
case "+":
{
d=Convert.ToDouble(lastValue) + Convert.ToDouble(val);
break;
}
case "-":
{
d=Convert.ToDouble(lastValue) - Convert.ToDouble(val);
break;
}
case "*":
{
d=Convert.ToDouble(lastValue) * Convert.ToDouble(val);
break;
}
case "/":
{
if (Convert.ToDouble(val) != 0.0)
{
d=Convert.ToDouble(lastValue) / Convert.ToDouble(val);
}
break;
}
}
lastValue=d.ToString("N", formatProvider);
return lastValue;
}

public void OnAdd(object sender, EventArgs e)
{
if (lastValue==String.Empty)
{
lastValue=display.Text;
}
else
{
lastValue=ProcessLastOp(display.Text);
display.Text=lastValue;
}
lastOp="+";
cleared=true;
}

public void OnSubtract(object sender, EventArgs e)
{
if (lastValue==String.Empty)
{
lastValue=display.Text;
}
else
{
lastValue=ProcessLastOp(display.Text);
display.Text=lastValue;
}
lastOp="-";
cleared=true;
}

public void OnMultiply(object sender, EventArgs e)
{
if (lastValue==String.Empty)
{
lastValue=display.Text;
}
else
{
lastValue=ProcessLastOp(display.Text);
display.Text=lastValue;
}
lastOp="*";
cleared=true;
}

public void OnDivide(object sender, EventArgs e)
{
if (lastValue==String.Empty)
{
lastValue=display.Text;
}
else
{
lastValue=ProcessLastOp(display.Text);
display.Text=lastValue;
}
lastOp="/";
cleared=true;
}

public void OnEqual(object sender, EventArgs e)
{
if (lastValue != String.Empty)
{
display.Text=ProcessLastOp(display.Text);
lastValue=String.Empty;
lastOp=String.Empty;
cleared=true;
}
}
}
}


Where's The Smoke And Mirrors?


There's only one "trick" being employed here. Absolute positioning is used and Web controls don't support that except by using the style attribute in the HTML. .NET's support for this is via the Style property. However, this property does not accept a string that would be used to construct the HTML style attribute. Instead, the WebControl.Style property is CssStyleCollection type. To make matters worse, although this type behaves sort of like a dictionary (it has a key-value pair Add method), it isn't derived from IDictionary.


Therefore, the smoke and mirrors is that MyXaml has custom property setter code for the CssStyle attribute. When it encounters the CssStyle attribute, it invokes a handler for this attribute (CssStyleCustomProperty) that decodes the string and loads up the CssStyleCollection for the Style property of the instance being parsed. At least the mechanism that is used is a general purpose custom attribute handler, rather than embedding web style processing inside the MyXaml parser directly.


The Downloads


For the web demo:



  1. Create an ASP.NET web application project.
  2. Copy the the files in the web download into the folder for your project, overwriting the WebForm.* files.
  3. Add a reference to the MyXaml.dll assembly that got put into your bin directory.


For the form demo, just unzip the download and compile the provided csproj.


A Note To MyXaml Users


If you want to compile against the MyXaml source code, there's a minor change made to MyXaml to deal with the prefix on the MyXaml node. In InitializeNamespaces, change:

XmlNode uiNode=doc.GetElementsByTagName("MyXaml")[0];


to

XmlNode uiNode=doc.DocumentElement;


Because MyXaml is a GPL open source project, I would prefer that people obtain the source code directly from the MyXaml website and acknowledge that they agree to the licensing terms. The downloads include only the MyXaml assembly.


GAC Issues


MyXaml registers itself into the GAC, and for some very annoying reason, I had problems with the web version acquiring the correct MyXaml file. If you suspect such a problem, remove MyXaml from the GAC (note that the MyXaml project has a post-build step that registers itself into the GAC, so you might want to remove that too).


Conclusion


Within the confines of simple UI controls, simple requirements, and where post-back performance isn't an issue, declarative programming and XSLT makes it possible to create both form and web applets from the same code base.