关于一个Silverlight3的小项目总结
最近这段时间在为California的一家保险公司做外包项目,该公司的技术选型为MVC1.0+Silverlight3 +WCF+Multi DB(MS SqlServer2008+IBM DB2),从年初启动到现在半年过去了,项目基本上算是完了,正在最后测试准备上线之际。不过,我今天总结的并不是这个项目,而是在这个项目做到一半的时候,该公司想为他们的多个网站加个免费计算器的一个Silverlight 小工具。由我在Escrum上总用时差不多40个小时完成的,工具虽小,但五脏俱全,所以我还是做个小小的技术总结。
先说一下作用背景(按照意思的总结,并非翻译哈)吧:
1、可以很轻便的使用于不同的网站上,即无缝的挂接到现有的网站上去。
2、基于Silverlight3实现,因为Silverlight4好象只能在vs2010上做吧,貌似这样子。
3、需要尽快完成,一两周内就要提供上去。
4、尽可能的可以运行在不同浏览器上(这个听起来有点不对劲,多些一举的说法,不过,做到中间我还真遇到了复制至剪贴板不支持IE以外的情况。)
5、工具需要提供能够输入,复制剪贴板,打印,语音帮助,文字帮助功能。
6、一些业务规则和数据输出计算规则随后由BRD文档提供。
OK,就这些要求,看起来应该是蛮简单的。而事实上也确是如此,最终,我使用的是一个xaml文件,一个Model和一些辅助类来完成的这些。对,我选用的是MVVM模式来弄的。因为最初我还傻傻的以为他们最终会找个美工来实现界面漂亮点。从现在看来,对方公司好象对我做的页面也没有多大的意见,似乎也没请美工捉刀美化页面的意思,不过这些也不是重要的,重要的咱的技术总结:
1、要可以在不同的asp.net网站上运行,少不了要跟html页面打交道了,而html上的Dom控制的王者不言而喻的是Javascript了。Silverlight要和JavaScript交互使用,需要这样子来弄:
1-A,Silverlight调用Javascript方法:
直接在xaml.cs文件中使用
HtmlPage.Window.Eval("javascript:window.close();");//或者
HtmlPage.Window.Invoke("closePage");
上面closePage是在Javascript中定义的一个方法,不带参数的,如果带参数,也是可以的,实现做法一个样。另外上面的HtmlPage的完整类名叫System.Windows.Browser.HtmlPage。
1-B,Javascript调用Silverlight中的方法:
这个稍微要多点步骤了,首先是在xaml.cs的构造需要注册成scriptable对象
HtmlPage.RegisterScriptableObject("PrintPage", this);//这里的PrintPage可以理解为ID,Javascript靠这个来识别到哪一个类中找方法,所以定义一个好的命名是会在写Javascript时有很大帮助的。
然后还在要被调用的方法或属性的前面申明[ScriptableMember],当然要是想把整个类都公开给Javascript,也可以在类的前面申明为[ScriptableType],建议不该公开的东西还是别公开吧。
最后,在Javascript就可以使用这个方式来访问了:
fucntion getPropertyFromSilverlight(){}
var slCtrl=document.getElementById("silverlightControl"); //获得Silverlight控件
var page=slCtrl.Content.PringPage;//取得将要调用的方法的所在类。
alert(page.PrintText);//这里假设在xaml.cs类中定义了一个名为PrintText的public属性.
1-C,将一些Javascript方法或CSS写进C#中,这个花费过我一些时间,尤其是将CSS写入进去的时候,一定要写在div并且一定要在<style>前面加上一个<br/>才调成功,至于是否写成一行或者多行就没有关系了。下面这段代码是实现打印部分生成的文字的一段CSS代码
public class PrintHelper
{
static string StyleId = Guid.NewGuid().ToString("N");
public static void PrintText(string text)
{
var body = HtmlPage.Document.Body;
if (HtmlPage.Document.GetElementById(StyleId) == null)
{
var style = HtmlPage.Document.CreateElement("div");
style.SetAttribute("id", StyleId);
style.SetProperty("innerHtml", @"<br />
<style type='text/css'>
#printHost
{
display: none;
}
@media print
{
#form1
{
display: none;
}
#printHost
{
display: block;
}
}
</style>");
body.AppendChild(style);
}
//var obj = HtmlPage.Document.CreateElement("object");
//obj.SetAttribute("id", "wb");
//obj.SetAttribute("name", "wb");
//obj.SetAttribute("height", "0");
//obj.SetAttribute("width", "0");
//obj.SetAttribute("classid", "CLSID:8856F961-340A-11D0-A96B-00C04FD705A2");
var innerHtml = HtmlPage.Document.GetElementById("printHost");
if (innerHtml == null)
{
innerHtml = HtmlPage.Document.CreateElement("span");
innerHtml.SetAttribute("id", "printHost");
innerHtml.SetAttribute("name", "printHost");
innerHtml.SetProperty("innerHTML", text);
}
//body.AppendChild(obj);
body.AppendChild(innerHtml);
HtmlPage.Window.Eval("javascript:window.print();");
//HtmlPage.Window.Eval("javascript:var wb=document.getElementById('wb');wb.execwb(7, 1);");//print view
//HtmlPage.Window.Eval("javascript:var wb=document.getElementById('wb');wb.execwb(6, 1);");//print
//HtmlPage.Window.Eval("javascript:var wb=document.getElementById('wb');wb.execwb(8, 1);");//print page setup
}
}
以上我们约定aspx页面的Form id是form1。
1-D,也是想用C#调用clipboradData来复制文本至剪贴板。当然这段代码是复制于网上,不过我最终没有真正的去跨非IE浏览器,因为对方说可以不做这个,直接给个不支持非IE的信息就可以了。先贴段代码在这里吧,以免哪一天打不开上面这段链接。
public class ClipboardHelper
{
const string HostNoClipboard = "The clipboard isn't available in the current host.";
const string ClipboardFailure = "The text couldn't be copied into the clipboard.";
const string BeforeFlashCopy = "The text will now attempt to be copied...";
const string NotSupportBrowser = "The clipboard isn't available in the current host.";
const string FlashMimeType = "application/x-shockwave-flash";
// HARD-CODED!
const string ClipboardFlashMovie = "ZeroClipboard.swf";
/// <summary>
/// Write to the clipboard (IE and/or Flash)
/// </summary>
public static void SetText(string text)
{
// document.window.clipboardData.setData(format, data);
var clipboardData = (ScriptObject)HtmlPage.Window.GetProperty("clipboardData");
if (clipboardData != null)
{
bool success = (bool)clipboardData.Invoke("setData", "text", text);
if (!success)
{
HtmlPage.Window.Alert(ClipboardFailure);
}
}
else
{
HtmlPage.Window.Alert(NotSupportBrowser);
// Append a Flash embed element with the data encoded
string safeText = HttpUtility.UrlEncode(text);
var elem = HtmlPage.Document.CreateElement("div");
HtmlPage.Document.Body.AppendChild(elem);
elem.SetProperty("innerHTML", "<embed src=\"" +
ClipboardFlashMovie + "\" " +
"FlashVars=\"clipboard=" + safeText + "\" width=\"0\" " +
"height=\"0\" type=\"" + FlashMimeType + "\"></embed>");
}
}
}
2、如果你不希望你的Xaml文件中的Style啊Resource之类的充斥其中的话。你可以新建一个文件夹专门用来存放这些内容的,然后在App.xaml中合并这些,作为StaticResource来使用。
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Themes/PopupWindow.xaml"/>
<ResourceDictionary Source="Themes/Converter.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
很明显的,我是建了一个Themes的文件夹,里面加了两个ResourceDictionary,做了以上这些操作之后,如果在页面中,想使用Style也好,Converter也好,直接StaticResource Key就可以完成了。而不需要在页面中再申明了。这就是全局资源的好处,当然也不是定义全局资源就是好方法,页面资源就一无是处了,这个取舍还在于一个平衡度的问题。
3、讲到Converter了,我有一点深刻印象的是,要实现一个textBox的背景颜色的切换的问题,一般的直接用IValueConverter来实现的话,还不一定达到效果,我最后是在一位同事的指点下,利用一个集合来完成的。OK,先看我最终的IValueConverter吧:
public class StringToGrayColorConverter : DependencyObject,System.Windows.Data.IValueConverter
{
#region IValueConverter Members
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
//if (String.IsNullOrEmpty(value.ToString()))
// return Colors.White;
//if (value.ToString().ToLower() == "yes")
// return Colors.LightGray;
//else
// return Colors.White;
if (null == value)
{
throw new System.ArgumentNullException("value");
}
BrushCollection brushes = Brushes;
if (parameter != null)
brushes = (BrushCollection)parameter;
return (value.ToString().ToLower()=="yes") ? brushes[0] : brushes[1];
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
#endregion
public BrushCollection Brushes
{
get { return (BrushCollection)GetValue(BrushesProperty); }
set { SetValue(BrushesProperty, value); }
}
public static readonly DependencyProperty BrushesProperty =
DependencyProperty.Register("Brushes", typeof(BrushCollection), typeof(StringToGrayColorConverter), new PropertyMetadata(null));
}
public class BrushCollection : List<Brush>
{
}
然后,我们在Converter.xaml中如是定义,才可以使用:
<Color x:Key="DisableBackgroundColor">LightGray</Color>
<Color x:Key="EnableBackgroundColor">White</Color>
<Converters:StringToGrayColorConverter x:Key="stringToGrayColorConverter">
<Converters:StringToGrayColorConverter.Brushes>
<Converters:BrushCollection>
<SolidColorBrush Color="{StaticResource EnableBackgroundColor}" />
<SolidColorBrush Color="{StaticResource DisableBackgroundColor}" />
</Converters:BrushCollection>
</Converters:StringToGrayColorConverter.Brushes>
</Converters:StringToGrayColorConverter>
4、Behavior和Action
在VS2008中,如果有装Blend话,会有建Behavior或Action的模板,但不知为什么,在VS2010里面反倒没有了。不过,用Expression Studio4的话就还是有这些选项的。对于Triggers、Actions和Behavior,我还是在网上做了一些功课的。摘抄地址在这里:
Triggers、Actions 和Behaviors使得在Silverlight应用程序中进行交互操作变得更为容易,用XAML即可完成诸多功能,可以减去复写后台代码的烦恼,需要借助Blend 3 SDK的System.Windows.Interactivity.dll和Microsoft.Expression.Interactions.dll程序集。
Triggers和Actions是因果关系模型,一个触发器可以调用一个或多个操作,而Behaviors则大致相当于两者的一个小综合体。他们的类关系图如下:
所谓Trigger,就是监听某些条件的变化,比如事件触发,属性值改变等,进而触发一些动作的发生。这些Triggers可能是EventTrigger、CollisionTrigger 等,当然更多的或许是创建自己的Trigger。自定义Trigger只需要从TriggerBase<DependencyObject>继承,并覆盖OnAttached和OnDetaching方法即可。
所谓Action,就是去执行某些操作。可以根据需要创建自己的Action,常见的需要创建Action的情况有:改变属性、调用方法、打开窗口、导航到某个页面、设置焦点等。自定义Action可从 TriggerAction<DependencyObject>或TargetedTriggerAction<DependencyObject>继承,区别在于操作对象是关联对象还是特定的目标对象,实现时覆盖Invoke方法即可。
Triggers和Actions理论是可以相互独立,任意组合的。当你在定义时发现有些逻辑上需要相互确定或者假定发生时,Behaviors需要登台了。Behaviors乍看起来像是Actions,但它是逻辑独立功能自备的独立单元,它无需触发器,定义Behavior时就已经确定。
创建自定义Behavior需要从Behavior<DependencyObject>继承,//自定义行为默认继承Behavior<DependencyObject>使用DependencyObject类型的行为是不能访问对象的鼠标事件的,如果要访问鼠标操作的事件,可以使用具体的UI组件类型或者直接使用UI元素基类UIElement。//
并覆盖OnAttached和OnDetaching方法,复杂行为时需要用到ICommand. 当然在Blend 3中已经预定义了不少Behaviors,如MouseDragElementBehavior等。可以利用,同时在Expression Gallery 也可以共享他人或自己的Behavior。(PS:Effects 和 Themes也可以在这里共享。)
我在这个项目中,只使用了一个Action和一个Behavior,都作用于textBox上面,分别用来获得焦点时就全选文本和控制只能输入数字的作用。
首先来看Action:
public class TextboxAction : TargetedTriggerAction<TextBox>
{
public TextboxAction()
{
// Insert code required on object creation below this point.
}
protected override void Invoke(object o)
{
// Insert code that defines what the Action will do when triggered/invoked.
TextBox tbx = Target;
tbx.SelectAll();
}
}
然后看一下只允许输入数字的一个Behavior。
public class DigitalOnlyBehavior : Behavior<UIElement>
{
public DigitalOnlyBehavior()
{
// Insert code required on object creation below this point.
//
// The line of code below sets up the relationship between the command and the function
// to call. Uncomment the below line and add a reference to Microsoft.Expression.Interactions
// if you choose to use the commented out version of MyFunction and MyCommand instead of
// creating your own implementation.
//
// The documentation will provide you with an example of a simple command implementation
// you can use instead of using ActionCommand and referencing the Interactions assembly.
//
//this.MyCommand = new ActionCommand(this.MyFunction);
}
protected override void OnAttached()
{
base.OnAttached();
this.AssociatedObject.KeyDown += new KeyEventHandler(AssociatedObject_KeyDown);
// Insert code that you would want run when the Behavior is attached to an object.
}
void AssociatedObject_KeyDown(object sender, KeyEventArgs e)
{
char c = (char)e.PlatformKeyCode;
switch (e.PlatformKeyCode)
{
case 190:
case 110:
c = '.';
break;
case 96:
case 97:
case 98:
case 99:
case 100:
case 101:
case 102:
case 103:
case 104:
case 105:
c = IntToChar(e.PlatformKeyCode - 96);
break;
}
if (!Regex.IsMatch(c.ToString(), Filter))
{
e.Handled = true;
}
}
protected override void OnDetaching()
{
base.OnDetaching();
this.AssociatedObject.KeyDown -= AssociatedObject_KeyDown;
// Insert code that you would want run when the Behavior is removed from an object.
}
public static readonly DependencyProperty FilterProperty =
DependencyProperty.Register("Filter", typeof(string), typeof(DigitalOnlyBehavior), new PropertyMetadata(@".*"));
public string Filter
{
get { return (string)GetValue(FilterProperty); }
set { SetValue(FilterProperty, value); }
}
char IntToChar(int intN)
{
return (char)(intN + 48);
}
/*
public ICommand MyCommand
{
get;
private set;
}
private void MyFunction()
{
// Insert code that defines what the behavior will do when invoked.
}
*/
}
调用时分别使用:
<i:Interaction.Triggers>
<i:EventTrigger>
<actions:TextboxAction/>
</i:EventTrigger>
</i:Interaction.Triggers>
<i:Interaction.Behaviors>
<behaviors:DigitalOnlyBehavior Filter="{StaticResource DigitalOnlyFilter}"/>
</i:Interaction.Behaviors>
<!--DigitalOnlyFilter定义在我们前面介绍的Converter.xaml中
<system:String x:Key="DigitalOnlyFilter" >[0-9.\t]</system:String>
-->
5、验证,本小项目中,验证全部是在Property的Set中验证的。然后在xmal文件中使用Binding时加上以下两个属性,NotifyOnValidationError=true, ValidatesOnExceptions=true 这两个属性由于较长,一般容易写错,所以我特意挑出来放在这里作为总结。
6、ViewModel和View之间的沟通,在View构造时,将DataContext赋值为ViewModel即可。当然也可以写在Page Resource中,这是很多网上的一些教程使用的方法。
7、有个向下靠千取整的问题,目前我使用的是Math.Round的方法来实现的。我也知道,这个算法肯定是登不了大雅之堂的。如果哪位看到了这里,可否留言指教一下这个算法应该如何写最高效最简洁最优雅。
var temp = 总金额/ 总人数/ 1000;
if ((int)Math.Round(temp) == (int)temp)
平均金额= Math.Round(temp, 0) * 1000;
else
平均金额 = Math.Round(temp - 0.5m, 0) * 1000;
结论:其实,Silverlight使用起来也是蛮方便,也不是很难,特别是用MVVM这种模式来做的话,其实这个模式一大好处之一是跟MVC一样,可以让程序员更放心的交付可靠代码,分离程序员和美工的职责,让项目同步进行。做Silverlight的ViewModel没有多大的不同以往,注意Converter,Behavior,Action,Trigger这些,然后界面可能就要强调一下VSM,Animation等内容了。
后记:天下事,事与愿违之事有之,昨天接到邮件,该公司最终决定不用Silverlight来做这个小工具,搞着玩的嘛?我辛苦花了40多个小时的时间完成,其间也来来回回有过QA记录,难道到了最后,才发现用Silverlight不好?要改用传统的Asp.net才好?郁闷!不过,事还是要做的,按照他们的要求做成Asp.net的,OK,好,改就改吧。与Silverlight几点不同。
1、textBox要显示Currency的格式。这点不同于Silverlight。Silverlight只需写个Converter即可实现,而在Asp.net中,要实现的话,只能靠Javascript的onfocus和onblur事件来改变了。
function formatCurrencyTextBox(ctrl) {
ctrl.value = formatCurrencyValue(ctrl.value);
}
function formatCurrencyValue(amount) {
var delimiter = ","; // replace comma if desired
var a = amount.split('.', 2)
var d = a[1];
var i = parseInt(a[0]);
if (isNaN(i)) { return ''; }
var minus = '';
if (i < 0) { minus = '-'; }
i = Math.abs(i);
var n = new String(i);
var a = [];
while (n.length > 3) {
var nn = n.substr(n.length - 3);
a.unshift(nn);
n = n.substr(0, n.length - 3);
}
if (n.length > 0) { a.unshift(n); }
n = a.join(delimiter);
if (d == null || d.length < 1) { amount = n; }
else { amount = n + '.' + d; }
amount = minus + amount;
return "$" + amount;
}
function unFormatCurrencyTextBox(ctrl) {
ctrl.value = unFormatCurrencyValue(ctrl);
ctrl.select();
}
function unFormatCurrencyValue(ctrl) {
var value = ctrl.value.replace(new RegExp(',', 'g'), '');
value = value.replace(new RegExp('\\$', 'g'), '');
return value;
}
2、复制剪贴板和打印部分区域的。可以沿用原Silverlight的思路,因为asp.net的打印/复制的目标文字是写在Html标签中和Asp.net标签(动态内容)中的,而不像Silverlight时,直接在ViewModel中公开一个string属性,传过来给PrintHelper打印或CopyHelper复制即可。因此,需要在上面提到的这些标签的内容的前后加个识别标识,为不引起不必要的显示或岐义,加上<!--startprint-->共17个字符开始和<!--endprint-->字符结束。单单加个这个还没有什么作用的,需要用一段Javascript代码来获取这区间段内的文字
function getCopyPrintText() {
var strBody = window.document.body.innerHTML;
var strBegin = "<!--startprint-->";
var strEnd = "<!--endprint-->";
var strPrint = strBody.substr(strBody.indexOf(strBegin) + 17);
strPrint = strPrint.substring(0, strPrint.indexOf(strEnd));
return strPrint;
}
复制时,IE下直接这样子
clipboardData.setData("Text",getCopyPrintText());
然而,不知是什么原因,该公司对Firefox似乎很有情感,这也说明可能美国那边的Firefox的市场占有率还是蛮高的。要求这个asp.net的要支持Firefox,于是,复制的这段代码也被封装成了一个支持IE和Firefox的函数了
function copyToClipboard(s)
{
if( window.clipboardData && clipboardData.setData )
{
clipboardData.setData("Text", s);
}
else
{
// You have to sign the code to enable this or allow the action in about:config by changing
//("signed.applets.codebase_principal_support", true);
try{
netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
}
catch(e) {
alert("Access Denial!\nPlease enter 'about:config' in the address bar,\n and make sure the 'signed.applets.codebase_principal_support' is true");
}
var clip = Components.classes['@mozilla.org/widget/clipboard;1'].createInstance(Components.interfaces.nsIClipboard);
if (!clip) return;
// create a transferable
var trans = Components.classes['@mozilla.org/widget/transferable;1'].createInstance(Components.interfaces.nsITransferable);
if (!trans) return;
// specify the data we wish to handle. Plaintext in this case.
trans.addDataFlavor('text/unicode');
// To get the data from the transferable we need two new objects
var str = new Object();
var len = new Object();
var str = Components.classes["@mozilla.org/supports-string;1"].createInstance(Components.interfaces.nsISupportsString);
var copytext=s;
str.data=copytext;
trans.setTransferData("text/unicode",str,copytext.length*2);
var clipid=Components.interfaces.nsIClipboard;
if (!clip) return false;
clip.setData(trans,null,clipid.kGlobalClipboard);
}
}
然后复制也变成
copyToClipboard(RemoveHtmlTag(getCopyPrintText(), false));
上面的RemoveHtmlTag是一个去掉Html标签的脚本,在修改这段代码的时候,让我深感学好正则表达式的好处。当然,如果是在IE下,不用下面这个函数去掉标签,复制进clipboard之后,也是没有标签的,但firefox却有,为保一致,还是加上这个。
function RemoveHtmlTag(str, noEnter) {
var html = str;
html = html.replace(/^[ ]*/img, " "); //space
html = html.replace(/<!--[\s\S]*?-->/img, ""); //comment
html = html.replace(/<[\/]*table[^>]*>/img, "\n"); //table
html = html.replace(/<[\/]*tbody[^>]*>/img, ""); //tbody
html = html.replace(/<[\/]*tr[^>]*>/img, "\n"); //tr
html = html.replace(/<[\/]*td[^>]*>/img, "\n"); //td
html = html.replace(/<[\/]*p[^>]*>/img, "\n"); //p
html = html.replace(/<[\/]*a[^>]*>/img, ""); //a
html = html.replace(/<[\/]*col[^>]*>/img, "\n"); //col
html = html.replace(/<[\/]*br[^>]*>/img, "\n"); //br
html = html.replace(/<[\/]*[^>]*>/img, ""); //
html = html.replace(/<[\/]*span[^>]*>/img, ""); //span
html = html.replace(/<[\/]*center[^>]*>/img, ""); //center
html = html.replace(/<[\/]*ul[^>]*>/img, ""); //ul
html = html.replace(/<[\/]*i[^>]*>/img, ""); //i
html = html.replace(/<[\/]*li[^>]*>/img, ""); //li
html = html.replace(/<[\/]*b[^>]*>/img, ""); //b
html = html.replace(/<[\/]*hr[^>]*>/img, ""); //hr
html = html.replace(/<[\/]*h\d+[^>]*>/img, ""); //h1,2,3,4,5,6
html = html.replace(/<STYLE[\s\S]*?<\/STYLE>/img, ""); //style
html = html.replace(/<script[\s\S]*?<\/script>/img, ""); //reference script
//html = html.replace(/<[\?!A-Za-z\][^><]*>/img, "");alert("str:"+html)
html = html.replace(/\r/img, ""); //break
html = html.replace(/\n/img, "\r\n"); //enter
// html = html.replace(/[ |\s]*\r\n[ |\s]*\r\n/img, "\r\n");
if (noEnter) {
html = html.replace(/\r\n/img, "");
html = html.replace(/\n/img, "");
html = html.replace(/\r/img, "");
}
return (html);
}
至于思路同于Silverlight打印的那段代码,在原html里加个<div id="printHost"></div>,用CSS控制打印区域,当然Javascript需要给printHost区域赋值,然后用window.print()即可。
function printText() {
var printHost = document.getElementById("printHost");
printHost.innerHTML = getCopyPrintText();
window.print();
}
3、业务规则,跟Silverlight一样。
4、至于是否使用Ajax,就不总结记录了。
5、使用Ajax,如果要在cs后台中显示alert,需要这样子包装:
ScriptManager.RegisterStartupScript(up1,up1.GetType(),"zeroAmount","<script type='text/javascript'> alert('up1 is UpdatePanel.');</script>",false);
没有用Ajax的话,就这样子
Page.Response.Write("<script type='text/javascript'> alert('This is a test.');</script>");
就好了。
总结:Silverlight跟Asp.net还是有一些区别的,至少在实现一些细节的时候,Silverlight要方便些,比方说与Javascript之间的交互啊(因为都是客户端嘛),自定义显示啊等。
最后,希望这次做完之后,没有多少大的改变了,至少我想不会做成WPF了的,或者再回到Winform里去吧。
又续:决定升级silverlight4,需要安装vs2010,却出现:Error code 1601...,解决方法是msiexec /unreg和msiexec /regserver.据说2003类的用net stop msiserver和net start msiserver。