声明式编程——统一窗体和 Web 开发






4.69/5 (10投票s)
2004年11月23日
4分钟阅读

73945

914
使用声明式编程创建 Web 和窗体小程序通用的 UI。
引言
这是一篇概念性文章,旨在激发您对声明式编程的兴趣,并探讨是否有可能统一 System.Windows.Forms
和 System.Web.UI.WebControls
命名空间(至少对于简单应用程序而言)的开发。
当然,“真正的”网页包含大量 JavaScript 来减少回发,服务器端会进行大量优化,需要处理流量等实际问题。而且计算器示例,其中每个按钮点击都是一次回发,绝对不是最好的例子!
单击 此处 观看在线演示。
它是如何工作的?
非常简单。给定一个 MyXaml 标记文件,该文件在 System.Windows.Forms
命名空间中定义 UI,一个非常简单的 XSLT(由 Justin 编写)会将该标记转换为可以存在于 System.Web.UI.WebControls
命名空间中的标记。因此,一个标记文件可以驱动窗体和 Web UI,事件处理代码也相同,只有一个例外。
标记
源标记非常简单。没有样式,没有子窗体等。为了实现这一点,简单是最好的。
<?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>
生成的内容是什么?
XSLT 将其转换为
<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>
XSLT 看起来是什么样的?
XSLT 非常直接
<?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>
加载器
为了让所有这些魔法生效,标记必须被解析,这由 MyXaml 完成。解析在应用程序启动时完成。对于窗体版本,这在调用 Main()
时完成。在 Web 版本中,它在 Page_Load
事件处理程序中完成。
窗体加载器
窗体版本有一个非常简单的加载器
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);
}
}
}
Web 加载器
Web 加载器更复杂,因为没有会话状态的概念。因此,管理计算器状态(连接数字、记住最后一个运算符等)的类必须保存在 Session
容器中。此外,每次回发时,都必须重新生成 UI。
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;
}
}
}
计算器事件处理程序代码
计算器事件处理程序代码对于窗体和 Web 版本是相同的,只是在窗体版本中您必须指定
using System.Windows.Forms;
而在 Web 版本中,您指定
using System.Web.UI.WebControls;
这是 Web 版本
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;
}
}
}
}
烟雾和镜子在哪里?
这里只使用了一个“技巧”。绝对定位,而 Web 控件不支持这一点,除非使用 HTML 中的 style 属性。 .NET 对此的支持是通过 Style
属性。但是,此属性不接受用于构造 HTML style 属性的字符串。相反,WebControl.Style
属性是 CssStyleCollection
类型。更糟糕的是,虽然此类型表现得有点像字典(它有一个键值对 Add
方法),但它没有从 IDictionary
派生。
因此,烟雾和镜子在于 MyXaml 为 CssStyle
属性提供了自定义属性设置器代码。当它遇到 CssStyle
属性时,它会调用该属性的句柄(CssStyleCustomProperty
),该句柄会解码字符串并将 CssStyleCollection
加载到正在解析的实例的 Style
属性中。至少使用的机制是一个通用的自定义属性句柄,而不是将 Web 样式处理直接嵌入 MyXaml 解析器中。
下载内容
对于 Web 演示
- 创建一个 ASP.NET Web 应用程序项目。
- 将 Web 下载中的文件复制到项目的文件夹中,覆盖 WebForm.* 文件。
- 添加对已放入 bin 目录的 MyXaml.dll 程序集的引用。
对于窗体演示,只需解压缩下载文件并编译提供的 csproj。
致 MyXaml 用户的一封信
如果您想针对 MyXaml 源代码进行编译,MyXaml 有一个小的更改,用于处理 MyXaml 节点上的前缀。在 InitializeNamespaces
中,更改
XmlNode uiNode=doc.GetElementsByTagName("MyXaml")[0];
to
XmlNode uiNode=doc.DocumentElement;
由于 MyXaml 是一个 GPL 开源项目,我希望人们直接从 MyXaml 网站获取源代码,并同意许可条款。下载仅包含 MyXaml 程序集。
GAC 问题
MyXaml 将自身注册到 GAC,但出于某种非常恼人的原因,我在 Web 版本获取正确的 MyXaml 文件时遇到了问题。如果您怀疑存在此类问题,请从 GAC 中删除 MyXaml(请注意,MyXaml 项目有一个生成后步骤会将自身注册到 GAC,因此您可能也想将其删除)。
结论
在简单的 UI 控件、简单的需求以及回发性能不是问题的范围内,声明式编程和 XSLT 可以从同一个代码库创建窗体和 Web 小程序。