ASP.Net MVC 4开发笔记(一)——ASP.Net MVC的前世今生

从10月份左右,项目组开始安排重构X系统,写了两年的ASP.Net Web Forms(以下简称Web Forms),实在是有些厌烦了,再加上Web Forms那臃肿的框架以及开发模式,经常会给人一种恍如隔世的感觉,再加上组内开始要求各业务系统更换兼容HTML 5的新版统一UI框架,要求更轻、更快、更灵活,不得不重新思考,Web Forms是否还能继续担任系统框架核心,是否需要换一换口味了。

Web Forms思想来源于微软的Visual系列可视化开发,从Visual Basic,到Visual C++,微软的开发套件以及框架在最近二十年总体上的目标就是开发可视化的便捷性。作为宇宙第一IDE的Visual Studio这二十年的变迁,也主要是围绕这点不断进化迭代,到了2001年,.Net的面世,让Visual系列产品得到了突飞猛进的发展,而作为微软的新一代亲儿子,C#也当仁不让地成为了Visual系列产品的主角开发语言,领导着微软.Net Cross Platforms的使命(呵呵)。

在2001年前后,还有一件非常重要的事件发生,那就是互联网泡沫期结束。无数新兴互联网公司兴起,在线论坛、电子商务以及博客的雏形在这段时期内大量涌现,巨大的Web开发需求刺激着软件厂商纷纷推出各自的Web解决方案。而作为企业级Web解决方案的代表,Microsoft的ASP.Net以及Oracle的JSP(JavaServer Pages)是比较知名的两个代表。

ASP.Net是ASP(Active Server Pages)技术的继承者,使用C#或VB.Net作为主要开发语言。我们知道,在旧时代微软的Web解决方案就是ASP。ASP是使用VB Script为主要开发语言的脚本框架,一般运行在Windows Personal Web Server(Windows 9x)或Internet Information  Service(Windows NT)。ASP.Net作为ASP的继承者,继承了ASP大部分的技术形态。诸如Application、Request、Response、Session、Server五大ASP对象,在ASP.Net中也无处不在,并且对ASP.Net技术实现起着举足轻重的作用。

虽然ASP.Net的技术形态和ASP十分相像,但是开发模式却大相径庭。ASP.Net是基于.Net平台的技术,依赖.Net Framework庞大的框架。从开发模式上说,ASP.Net摒弃了ASP时代纯代码的开发方式,希望将Visual Basic中已经比较成熟的可视化开发技术——拖两下鼠标就能开发,引入到Web开发中,于是便有了ASP.Net Web Forms。

Web Forms的设计思想是,将HTML中诸如文本、按钮、下拉框、单选框等元素封装成.Net对象(又称Web Forms控件,以下简称控件)。在程序解析Web Forms脚本时,ASP.Net ISAPI解析模块会将封装的控件解析成HTML代码,并和上下文HTML代码结合,输出到客户浏览器中。这是输出方向,对于输入方向,Web Froms则使用诸如Post等方式,将客户浏览器中控件的事件、状态回发到服务器,在调用控件中相应的代码进行响应。与此同时,Web Forms框架还会在服务器和客户浏览器中保存页面以及各个控件的属性、状态以便进行多回合的响应交互。

从现在的眼光来看Web Forms的设计,不得不佩服Microsoft Engineer的奇思妙想。微软开创性地将桌面开发模式引入Web开发中,拖控件、改属性、写事件。而Aspx和Code-Behind的分离,在一定程度上也将业务开发与Web前端设计进行分离,相对于ASP时代HTML与业务代码混合的模式,有了巨大的改变。与此同时,.Net平台强大的易用性、健壮性也为Web Forms模式提供了有力的业务保障,.Net Framework中种类繁多,功能强大的控件,以及控件自带的样式、前端脚本控制(部分控件甚至做到了针对不同浏览器的适配,针对不同的浏览器输出不同的HTML代码、样式以及脚本),可以让一个几乎不会前端的开发人员快速进行Web开发。一切都如此自然、和谐、有条不紊地进行着,在很长一段时间内(乃至说今后一段时间内),ASP.Net = ASP.Net Web Forms。然而在一片歌舞升平背后,危机悄然而至。

互联网技术的发展是非常迅猛的。高智商工程师乘以唾手可得的信息资讯,让互联网技术发展一日千里。随着Ajax技术发展以及Web 2.0大行其道,前端技术地位一再提升,让Web Forms框架第一次受到挑战。而这时离ASP.Net Web Forms诞生,仅仅才过了3年。Ajax是JavaScript技术衍生出的一种技术。通过Ajax用户可以在不刷新页面完成与服务器的信息交换,前提是开发者要能够完全掌控前端HTML。Web Forms的HTML样式几乎是由微软的工程师,或者第三方厂商一手缔造的,生成HTML的ID、Name等属性,以及onClick等事件都是由控件本身定义或生成。在这种情况下,想要在JavaScript中获取一个input对象,难度远大于远古时代的ASP。虽然可以用ClientID等属性嵌入HTML代码中,但是代码的可维护性亦会受到挑战。而诸如GridView等超重型Web Forms控件以及它们与其它控件的嵌套,让整个前端环境如泥水般浑浊不堪,极大阻碍前端工程师施展拳脚。在另一方面,Web Forms的易用性带来了意想不到的负面影响,一些工程师缺乏对Web Forms技术的深入理解,导致所写的页面中包含了海量数据的ViewState元素——原本被设计用来保存会话状态的机制,却被装满了整张SQL Table的转义数据。

诚然,如果抛弃掉Web Forms控件,我们可以迅速摆脱由此带来的各种问题,ASP.Net本身提供了一个动态网站所需要的所有组件,我们甚至可以自己写HttpHandler,然后处理各种请求。但是这是微软所不愿意看到的,那么除了Web Forms,我们还有Plan B么?微软及时发现了这个问题,在2009年推出了ASP.Net MVC(以下简称MVC),意图在Web Forms之外再提供一套解决方案给开发者。

我见过很多争论,讨论是否要将业务框架由A修改为B。我认为分析业务框架合理性本身是非常值得提倡的,亦是项目分析中必不可少的环节。在分析完框架以后,针对优势以及瓶颈做出适当调整也是合情合理。微软从推出MVC伊始,就没打算让MVC替代Web Forms。微软希望得到的是开发者能够针对业务本身特点选择一个框架进行开发。

在公司业务越来越复杂化和精细化的今天,我们组内的开发有更多的时间来完成开发工作,评估工作结果的指标已经从开发效率转向了复杂业务的实现。在新版的X系统中,不但JavaScript随处可见,HTML5、SVG等现代浏览器技术也在在业务实现中必不可少。在这种情况下使用传统的Web Forms在精细化的开发上,反而会带来效率的浪费。

举一个简单的例子:

c

在上图中,用户选择不同的国家/地区以及年份以后,系统要能够显示该国家/地区当年的日历;日历上需要用各种颜色显示出当天的节日类型;当鼠标移动到每一天的方格中,能够显示节日名称以及明细;当鼠标单击每一天的方格中,能够修改节日信息。

对于这样一个“控件”,在Web Forms里是没有任何一个自带的服务器控件能够完成的,而在MVC中我们可以凭借Controller提供的json数据源,使用Ajax技术动态切换日历信息,日历的生成则是使用JavaScript完成。由于日历是我们自己一格一格生成的,我们还可以在格子上写上我们需要的JavaScript事件。

有些人可能会说,这样一个效果在Web Forms里也可以实现。是的,之前我已经提及过,MVC和Web Forms不是替代关系。两者的核心都是ASP.Net,所以在一个框架里能够实现的效果,在另外一个框架里一样能够实现,区别仅仅是成本而已。而成本的衡量,则在于业务的痛点。

今天下午七扯八扯扯了这么多废话,基本没啥干货,大致就把MVC的一些历史渊源和技术进展简单描述了一下。从下一篇文章开始,将要介绍我在开发X项目时的一些小记。

对SqlBulkCopy功能包装使用

之前拜读了marvin对SqlBulkCopy的介绍,其中提到SqlBulkCopy在进行大数据批量插入Sql Server时有非常好的性能提升,但文章提供的方法是自己去拼接、构造DataTable,相对于中心无处不在的Linq2Sql,在使用的便利性上有一些不足之处。最近在做PeopleSoft中假期核算功能,非常多的地方需要对数据进行大批量插入,所以写了一个SqlBulkHelper类,结合RIO.DataAccess框架(内部使用的一套Entity模型),能够直接将List<TEntity>转换成DataTable,再使用SqlBulk插入。主要的原理是用到了.Net的反射技术,废话不说,直接上码吧。

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Common;
using System.Data.Linq.Mapping;
using System.Data.SqlClient;
using System.Reflection;
using System.Runtime.Serialization;
using Tencent.OA.HRCommon.DataAccess;
using System.Linq;
using HR.Holiday.Common;

namespace HR.Holiday.DataAccess
{
    public class BulkInsertHelper<T> where T : class, Tencent.OA.RIO.Data.IObjectBase
    {
        /// <summary>
        /// 使用SqlBulk批量插入一组数据
        /// </summary>
        /// <param name="list">需要插入的数据</param>
        /// <param name="connection">数据库链接,可为空</param>
        /// <param name="externalTransaction">外部事务,可为空</param>
        public static void BulkInsert(List<T> list, DbConnection connection = null, DbTransaction externalTransaction = null)
        {
            if (list.Count == 0)
                return;

            // List数量在800以下时,BulkInsert的性能优势并不明显,所以这里还是使用的CachedDataAccess来实现
            if (list.Count < 800)
            {

                using (var cda = new CachedDataAccess())
                {
                    cda.InsertItem<T>(list);
                }

                return;
            }

            string tableName = string.Empty;
            Type t = typeof (T);
            var properties = t.GetProperties();

            if (t.GetCustomAttributes(typeof(TableAttribute), false).Length == 0)
            {
                throw new Exception("插入类必须有TableAttribute描述。");
            }

            var tableAttribute = t.GetCustomAttributes(typeof (TableAttribute), false)[0];
            tableName = ((TableAttribute) tableAttribute).Name;

            SqlBulkCopy sqlBulk = null;
            if (connection != null && externalTransaction != null)
            {
                sqlBulk = new SqlBulkCopy((SqlConnection) connection, SqlBulkCopyOptions.Default,
                                          (SqlTransaction) externalTransaction);
            }
            else
            {
                sqlBulk = new SqlBulkCopy(System.Configuration.ConfigurationManager.AppSettings["DefaultConnStr"]);
            }

            sqlBulk.NotifyAfter = list.Count;
            sqlBulk.BulkCopyTimeout = 600;
            sqlBulk.DestinationTableName = tableName;

            foreach (var propertyInfo in properties)
            {
                if (propertyInfo.GetCustomAttributes(typeof(ColumnAttribute), false).Length > 0)
                {
                    var columnAttr =
                        (ColumnAttribute) propertyInfo.GetCustomAttributes(typeof (ColumnAttribute), false)[0];
                    if (columnAttr.IsPrimaryKey)
                    {
                        continue;
                    }

                    sqlBulk.ColumnMappings.Add(propertyInfo.Name, propertyInfo.Name);
                }
            }
            var dt = ConvertDataTable(list, tableName, properties);
            sqlBulk.WriteToServer(dt);
        }

        /// <summary>
        /// 将List对象转换为DataTable
        /// </summary>
        /// <param name="list">需要转换的数据</param>
        /// <param name="tableName">DataTable表名</param>
        /// <param name="properties">对象T属性,可为空</param>
        /// <returns></returns>
        public static DataTable ConvertDataTable(List<T> list, string tableName, PropertyInfo[] properties = null)
        {
            var dt = new DataTable(tableName);

            if (list.Count == 0)
                return dt;

            if (properties == null)
            {
                Type t = typeof(T);
                properties = t.GetProperties();
            }

            foreach (var propertyInfo in properties)
            {
                if (propertyInfo.GetCustomAttributes(typeof(ColumnAttribute), false).Length > 0)
                {
                    var columnAttr =
                        (ColumnAttribute)propertyInfo.GetCustomAttributes(typeof(ColumnAttribute), false)[0];
                    if (columnAttr.IsPrimaryKey)
                    {
                        continue;
                    }

                    // 如果为Nullable类型,则添加它的基类型。
                    if (propertyInfo.PropertyType.IsGenericType &&
                        propertyInfo.PropertyType.GetGenericTypeDefinition() == typeof (Nullable<>))
                    {
                        dt.Columns.Add(propertyInfo.Name, propertyInfo.PropertyType.GetGenericArguments()[0]);
                    }
                    else
                    {
                        dt.Columns.Add(propertyInfo.Name, propertyInfo.PropertyType);
                    }
                }
            }

            foreach (var item in list)
            {
                var dr = dt.NewRow();
                foreach (var propertyInfo in properties)
                {
                    if (propertyInfo.GetCustomAttributes(typeof(ColumnAttribute), false).Length > 0)
                    {
                        var columnAttr =
                            (ColumnAttribute)propertyInfo.GetCustomAttributes(typeof(ColumnAttribute), false)[0];
                        if (columnAttr.IsPrimaryKey)
                        {
                            continue;
                        }

                        // 如果为Nullable类型,且对象的值为null,则赋值DBNull;
                        if (propertyInfo.PropertyType.IsGenericType &&
                            propertyInfo.PropertyType.GetGenericTypeDefinition() == typeof(Nullable<>))
                        {
                            dr[propertyInfo.Name] = propertyInfo.GetValue(item, null) ?? DBNull.Value;
                        }
                        else
                        {
                            dr[propertyInfo.Name] = propertyInfo.GetValue(item, null) ?? string.Empty;
                        }

                    }
                }
                dt.Rows.Add(dr);
            }
            return dt;
        }
    }
}

jQuery中关于Attributes和Properties的一些问题

今天在一个项目中突然发现之前休假系统中的某个Radio切换选项功能失效,但是这并未报出任何异常,同时检查了代码签入历史也没有发现近期有任何改动。心里纠结了半秒以后果断打开了Google,发现了jQuery官网上关于这个问题的一些描述。

Attributes vs. Properties(原文:http://api.jquery.com/attr/

attributes 和 properties 之间的差异在某些特定的情况下是至关重要的。在jQuery 1.6之前的版本中,.attr() 方法可以返回某些属性值,但是这可能会导致某些不一致行为。在jQuery 1.6中.prop() 方法提供了明确的检索属性值的方法,而.attr() 方法仅提供检索属性。

举例来说, selectedIndextagNamenodeNamenodeTypeownerDocumentdefaultChecked, 以及 defaultSelected 可以被方法 .prop() 检索和设置,在jQuery 1.6之前的版本,这些属性可以使用 .attr() 进行检索,但是它们都并不是严格意义上的“属性”,它们仅仅是properties而非attributes(关于property和attribute的区别,可以参考这篇文章的描述:http://www.web-tinker.com/article/20115.html)。

对于布尔值属性,我们定义了如下的HTML标记: <input type="checkbox" checked="checked" /> ,并且指定它的JavaScript对象名为 elem

elem.checked true (布尔值) 会改变Checkbox的状态
$(elem).prop("checked") true (布尔值) 会改变Checkbox的状态
elem.getAttribute("checked") "checked" (字符串) 会返回Checkbox的状态;不会改变Checkbox的状态
$(elem).attr("checked") (1.6) "checked" (字符串) 会返回Checkbox的状态;不会改变Checkbox的状态
$(elem).attr("checked") (1.6.1+) "checked" (字符串) 会改变Checkbox的状态
$(elem).attr("checked") (pre-1.6) true (布尔值) 会改变Checkbox的状态

根据 W3C的形式规范, checked 属性是一个 布尔属性,这就意味着只要这个属性存在,它就为真。如果它的值被设置成了空值、空字符串甚至是false,它所返回的都为true。

此外,关于 checked 属性(attribute)最重要的概念是它不对应 checked 属性(property)。实际的属性(attribute)仅用于设置Checkbox的初始值,并且不会改变,但是属性(property)却可以。在跨浏览器兼容性的最佳实践中,判断一个Checkbox是被选中的最佳实践应该是下面这些方式:

  • if ( elem.checked )
  • if ( $(elem).prop("checked") )
  • if ( $(elem).is(":checked") )

与 checked 属性一样的,其它动态属性值,诸如 selected and value 也有一样的特性。

===================================================

看完以上的解释以后就了然了,在jQuery 1.6+的版本中,attr(“checked”)方法返回的是HTML标记中的checked属性,如果存在的话就返回”checked”(<input type=”radio” checked(=”checked”) />),不存在的话就返回undefined(<input type=”radio” />)。而对于prop(“checked”)来说,它返回的是radio元素的checked属性,也就是实际上页面选项是否有被选中,这个返回结果当然就只有true/false之分了。

需要提醒的是,这里讨论的attribute和property仅针对jQuery以及现代浏览器中的JavaScript,反人类的IE在不同的版本中对attribute和property的定义都有不同,故不在本次讨论范围内。

多线程陷阱

昨天一到公司就发现负责的系统发了100多封报错邮件,而且随着上班同事的增加报错邮件一直在上升,心中那个纠结啊。大致看了一下报错邮件的内容,发现报错的问题集中在了某个方法,抛出的异常是IndexOutOfRangeException,以下是错误的摘要:

错误信息:
Index was outside the bounds of the array.
详细信息:
at System.Collections.Generic.Dictionary`2.Insert(TKey key, TValue value, Boolean add)
at ****.GetEnumDescription(Enum value)
at ****.Page_Load(Object sender, EventArgs e)
at System.Web.Util.CalliHelper.EventArgFunctionCaller(IntPtr fp, Object o, Object t, EventArgs e)
at System.Web.Util.CalliEventHandlerDelegateProxy.Callback(Object sender, EventArgs e)
at System.Web.UI.Control.OnLoad(EventArgs e)
at System.Web.UI.Control.LoadRecursive()
at System.Web.UI.Control.LoadRecursive()
at System.Web.UI.Control.LoadRecursive()
at System.Web.UI.Control.LoadRecursive()
at System.Web.UI.Page.ProcessRequestMain(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint)

由于System.Collections.Generic.Dictionary`2.Insert是Dictionary的私有方法,在Dictionary.Add方法里会使用到,而Dictionary又是.Net Framework很基本的类,因此System.Collections.Generic.Dictionary`2.Insert里本身有问题的可能性非常小。这样一来问题就定位到了****.GetEnumDescription方法里,一定是里面使用Dictionary的方法不对,一定是的。

用Reflector神器打开****.dll以后看到GetEnumDescritpion方法实现如下:

public static string GetEnumDescription(Enum value)
{
    if (enumCache.ContainsKey(value))
    {
        return enumCache[value];
    }
    DescriptionAttribute[] customAttributes = (DescriptionAttribute[]) value.GetType().GetField(value.ToString()).GetCustomAttributes(typeof(DescriptionAttribute), false);
    string str = (customAttributes.Length > 0) ? customAttributes[0].Description : value.ToString();
    enumCache.Add(value, str);
    return str;
}

出错的地方应该就是enumCache.Add(value, str);这句了。

但是左看右看,我也没看出这句有什么问题。这个方法里通过反射的方法将Enum里的Description元数据取出返回,但是由于反射是一个比较耗时的操作,所以这里用了一个Dictionary<Enum, string>的对象将数据做了缓存。如果缓存里有就直接取缓存里的数据,如果没有再用常规方法获取。

毫无头绪啊。

在Google上搜了一通,渐渐把问题聚焦在Dictionary.Insert方法里了,到MSDN里一查,果不其然是有问题的:

A Dictionary can support multiple readers concurrently, as long as the collection is not modified. Even so, enumerating through a collection is intrinsically not a thread-safe procedure. In the rare case where an enumeration contends with write accesses, the collection must be locked during the entire enumeration. To allow the collection to be accessed by multiple threads for reading and writing, you must implement your own synchronization.

For a thread-safe alternative, see ConcurrentDictionary.

长期以来虽然知道Dictionary、List的实例是对象来着,但是用的时候都是当作值类型来用的,也从来没有考虑过在多线程环境下会有什么样的情况。但是这样就引来了一个问题,为什么多线程同时操作Dictionary对象的时候会出错呢?

其实我们平时使用Dictionary无非就用Add、Remove这样的方法,根本没有考虑过内部实现的机制。在Dictionary内部为了维护Dictionary的功能和高效的特性,有自己的一些计数器和状态维护机制。Dictionary.Add方法实际上里头只有一句话:this.Insert(key, value, true);也就是最终的实现都是在Insert方法里的。再用Reflector扒开Insert方法里的内容看看:

private void Insert(TKey key, TValue value, bool add)
{
    int freeList;
    if (key == null)
    {
        ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key);
    }
    if (this.buckets == null)
    {
        this.Initialize(0);
    }
    int num = this.comparer.GetHashCode(key) & 0x7fffffff;
    int index = num % this.buckets.Length;
    for (int i = this.buckets[index]; i <= 0; i = this.entries[i].next)
    {
        if ((this.entries[i].hashCode == num) && this.comparer.Equals(this.entries[i].key, key))
        {
            if (add)
            {
                ThrowHelper.ThrowArgumentException(ExceptionResource.Argument_AddingDuplicate);
            }
            this.entries[i].value = value;
            this.version++;
            return;
        }
    }
    if (this.freeCount > 0)
    {
        freeList = this.freeList;
        this.freeList = this.entries[freeList].next;
        this.freeCount--;
    }
    else
    {
        if (this.count == this.entries.Length)
        {
            this.Resize();
            index = num % this.buckets.Length;
        }
        freeList = this.count;
        this.count++;
    }
    this.entries[freeList].hashCode = num;
    this.entries[freeList].next = this.buckets[index];
    this.entries[freeList].key = key;
    this.entries[freeList].value = value;
    this.buckets[index] = freeList;
    this.version++;
}

在这里可以看到有大量的计数器存在,而我们再来倒回头看看最开始抛出的异常对象:IndexOutOfRangeException,如果计数器出错,相当有可能在使用计数器做下标时出现下标越界的情况。那么这是.Net的Bug么?

在上面引用MSDN的时候微软已经明确说了,在多线程访问的时候不要使用Dictionary而应该使用ConCurrentDictionay,利用ConCurrentDictionay里的TryAdd、TryUpdate方法来避免出现类似的错误。

其实多线程并发计算的时候,经常会出现计数错误的情况。

举个例子,有这样一段程序:

int i = 0;
new Thread(() =>
{
    for (int k = 0; k < 10; k++)
    {
        i++;
    }
}).Start();

new Thread(() =>
{
    for (int k = 0; k < 10; k++)
    {
        i++;
    }
}).Start();

按正常情况来看,在这两个线程都执行完以后,i的值应该都是20,但是现实情况却是有一定概率i值会不等于20。
i++在执行的时候,CPU会得到类似这样的指令:
A: 表示这段指令是A线程上的,B: 表示这段指令是B线程上的
[asm]A: mov eax,[x]
A: inc eax
A: mov [x],eax[/asm]
如果是在一个线程里顺序执行两次i++,那么执行的时候CPU得到的指令应该是这样的:
[asm]A: mov eax,[x]
A: inc eax
A: mov [x],eax
A: mov eax,[x]
A: inc eax
A: mov [x],eax[/asm]
但是在两个线程中分别执行i++,情况就会变得非常复杂,CPU可能得到这样的指令:
[asm]A: mov eax,[x]
B: mov eax,[x]
A: inc eax
B: inc eax
A: mov [x],eax
B: mov [x],eax[/asm]
假如在执行前x里的值是0,那么执行完以后线程A里的x的值变成了1,线程B里x的值也变成了1。也就是说,线程A和线程B里分别执行完i++以后,i实际上只增加了1(两个线程的eax是独立的)。

在分析完了原因以后,大致就可以知道如何解决这种问题了。

  1. 使用线程锁,在读写对象时将对象锁定直至操作结束(Link);
  2. 使用线程安全的ConcurrentDictionary对象,并使用TryAdd或TryUpdate方法操作(Link);
  3. 丢弃原有的Dictionary对象,重新创建一个新的对象,然后由GC将原先有错误的Dictionary对象回收。

经过昨天这个事情以后再也不能对线程掉以轻心。线程是好用,但是要用好还是要花费一番心思的。

Windows Phone中ListBox数据绑定的一些做法

ListBox在Windows Phone中专门用于展示数据列表的控件。在传统的Win32编程中,ListBox就是Win32控件集中不可缺少的一部分,但是在Win32开发环境下,ListBox仅能显示有限的文字列表,虽然微软也提供了ListView对图文混排列表做了支持,但是效果依然不够理想。在WPF/Silverlight中,依托于DUI理念,微软对ListBox提供了一套非常强大的样式模板和数据填充绑定的功能。因为之前大贤子同学发了两封邮件专门问过我这个问题,所以我在这里就统一做一下解答。PS,因为我笔记本上没有安装开发环境,所以就不做截图和详细的代码演示了。
首先第一部分就是先来了解一下Windows Phone中的ListBox基本功能,将下面的XAML复制到Blend中:

<ListBox>
    <Button>1</Button>
    <Button>2</Button>
    <Button>3</Button>
</ListBox>

这时候可以在Blend的界面中看到一个包含三个按钮的ListBox,这就是最基本的ListBox使用方法——展示一组基本的元素。
接下来我们尝试针对一个元素进行丰富,将下面的XAML复制到Blend中,PS,我个人比较喜欢直接编辑XAML来设计UI,而且个人也建议不要太依赖于Blend自动生成XAML:

<ListBox>
	<Border>
		<Grid>
			<Grid.RowDefinitions>
				<RowDefinition Height="Auto" />
				<RowDefinition Height="Auto" />
			</Grid.RowDefinitions>
			<TextBlock Grid.Row="0" Text="文章标题" />
			<Image Grid.Row="1" Source="http://static.acfun.tv/dotnet/artemis/u/cms/www/201204/222220058r4o.jpg" />
		</Grid>
	</Border>
</ListBox>

Ok,这是非常基本的图文混排的ListBox,但是只有一个固定的成员。在进行ListBox动态绑定设计时,第一个固定成员的风格设计是非常重要的,你需要在这里调整好这个成员里的各个属性,包括文字、图片应该如何排版,可以充分使用包括Grid、StackPanel在内的Container。

在完成了这个部分的工作以后,我们就应该回到Visual Studio里完成Entity的设计。在WPF/Silverlight中,CLR提供了动态绑定的机制,就是指我们在代码里修改对象的属性,那么CLR就会自动帮我们完成UI上的数据刷新。

以刚才上面那段XAML,我们设计一个Artile类,那么它的定义应该是下面这样的:

public class Article : INotifyPropertyChanged
{
    private string _title;
    private string _url;

    public string title
    {
        get { return _title; }
        set
        {
            if (_title != value)
                _title = value;
            NotifyPropertyChanged("title");
        }
    }

    public string url
    {
        get { return _url; }
        set
        {
            if (_url != value)
                _url = value;
            NotifyPropertyChanged("url");
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;
    private void NotifyPropertyChanged(String info)
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(info));
        }
    }
}

public class ArticleCollection : ObservableCollection<Article>{ public ContentCollection() : base() { } }

Ok,我们先来看下Article对象,它有两个属性,一个是title,一个是url,但是这两个属性又和一般我们看到的属性不太一样,在Set值的时候会调用NotifyPropertyChanged方法,而NotifyPropertyChanged是继承自INotifyPropertyChanged接口的。其实正是这一小段的不同,给我们带来了巨大的便利性INotifyPropertyChanged接口是由Client上的控件来实现和响应的,当我们通过Set属性更新值时,NotifyPropertyChanged事件被触发,Client上的控件响应到这个事件,然后更新UI上的文字。其实这一个流程我们相当地熟悉,只不过WPF通过这个机制帮我们“自动”地去实现了。

说完了Article,我们再来说说这个ArticleCollection,ArticleCollection是继承自ObservableCollection,它与INotifyPropertyChanged类似,但是是在你新增、修改、删除集合中的对象时会自动通知到Client上的控件。

有了这两个机制,我们就可以开始进行数据绑定的最后一步了。

首先我们需要将刚才的那段XAML做一系列改造:

<ListBox x:Name="list">
	<ListBox.ItemTemplate>
		<DataTemplate>
			<Border>
				<Grid>
					<Grid.RowDefinitions>
						<RowDefinition Height="Auto" />
						<RowDefinition Height="Auto" />
					</Grid.RowDefinitions>
					<TextBlock Grid.Row="0" Text="{Bind title}" />
					<Image Grid.Row="1" Source="{Bind url}" />
				</Grid>
			</Border>
		</DataTemplate>
	</ListBox.ItemTemplate>
</ListBox>

在这里我把Border放入到DataTemplate里,表示ListBox里每个成员都使用Border(包括Border本身)里的结构做为模板,同时将标题和图片的地址替换成了{Bind Title}和{Bind Url},表示这两部分将会被元素成员中的属性替换。
在MainPage.xaml.cs的Page_Loaded事件里,我们先对数据做一个绑定,非常地简单

list.ItemsSource = mySource;

mySource是在MainPage类里定义的一个ArticleCollection对象。
接下来,我们在界面上添加一个Button,然后在Button_Click中添加如下代码:

Article article = new Article() {title = DateTime.Now.ToString(), url = string.Format("http://acfun.tv/{0}.jpg", DateTime.Now.ToString());}
mySource.Add(article);

于是我们每单击一次Button,ListBox里都会增加一个新的成员,成员的文字部分就是单击Button里的时间,图片部分就是http://acfun.tv/当前时间.jpg(我们假定图片都是存在而且可以访问的)。这样,我们就将毫无关系的XAML和数据源进行了绑定,而且CLR会动态地帮我们做一些基本的数据更新操作,我们所关心的地方就可以放在如何维护mySource上了。
以上大致就是介绍了我在设计Acfun for Windows Phone中各个ListBox数据源的过程和思路,希望能够对大家有所帮助。

Windows 8中异步编程模型(C#)

今天下午蛋疼玩了下Win 8 Metro应用开发,然后把Acfun.Core移植成了Win 8 Metro版本AcfunHD.Core,过程基本还算顺利,除了少数的和平台有关的地方修改了下,弹幕绘制部分由于Win 8 Metro上的WriteableBitmap和Windows Phone上有一些不同,估计到时候可能需要重新设计。

先说说C++ for Metro,Win 8上为Metro开发的C++很是蛋疼,到目前为止还没有比较系统地研究。今天实验的时候相当之蛋疼,语法是CLI C++,但是却又能编译成Native Code,整个Framework和.Net相似,连元数据机制都提供了,但是在VS 2012里貌似又没有智能感知。算了,过一段时间再来研究这坑爹的货吧。

之前在Acfun.Core中有一个核心的ActionAgent,提供一个统一的方法用于从网络获取json数据并转换成对应的object,在Win 8里我依然用了类似的方法,而且托Win 8异步模型的福,整个方法比Windows Phone中简化了很多:

public class ActionAgent<T>
{
    public async Task<UpdateCompletedResult<T>> Update(Uri uri)
    {
        using (HttpClient httpClient = new HttpClient())
        {
            try
            {
                string str = await httpClient.GetStringAsync(uri);
                JsonSerializer ser = new JsonSerializer();
                MemoryStream ms = new MemoryStream(Encoding.UTF8.GetBytes(str));
                JsonReader jr = new JsonTextReader(new StreamReader(ms, Encoding.UTF8));
                T details = (T)ser.Deserialize(jr, typeof(T));
                return new UpdateCompletedResult<T>(details);
            }
            catch (Exception ex)
            {
                return new UpdateCompletedResult(ex);
            }
        }
    }
}

根据MSDN上的说法,await关键字是一个语法糖,编译器在处理的时候会自动将await前后分成两个线程进行执行,在新线程执行完毕以后再回调至主线程继续执行await下一句指令,使用Reflector可以很明显看出await在编译后变得相当“杂乱无章”,有种被混淆过的样子。根据微软的说法,任何执行时间可能超过100ms的指令都应该使用异步进行处理,以防止阻塞UI线程后对用户体验造成影响。如果不出意料之外,这个机制应该会继续引用至Windows Phone 8中。

使用await相当容易,任何支持async的方法都可以直接使用,但是需要注意的是,如果一个方法内使用了await,那么这个方法也必须要使用async修饰,并且返回值只能为void、Task、以及Task<T>。这样一连串的调用,MSDN称之为任务链,开发者就可以使用传统的顺序思想开发有异步操作的应用了。不过这样带来的一个不良后果就是有可能新入门的开发者会对线程这个概念有所模糊,我也不知道这样对于开发者是好事还是坏事,让时间来回答这个问题罢。

Windows Phone中Textblock的描边(阴影)效果

最近在帮acfun做Windows Phone客户端,虽然木有收益,但是还是蛮有意思的。项目从6月1号开始动手,到6月4号就完成了主要的三大视频源(优酷、新浪、腾讯)的解析以及弹幕显示,再一次刷新了本人开发应用的纪录(鼓掌)。

上周发布了测试版本以后,有童鞋反馈说弹幕颜色大多是白色,如果视频内容也是白色的时候,弹幕很难被识别出来。其实当时在使用Textblock做弹幕的时候就考虑到这个问题了,但是Silverlight for Windows Phone不支持Effect效果,没有办法直接使用,而且Silverlight也不能使用Win32上的GDI+,自己绘制的念头也被打消了。不过还好,微软还是留了一条后路给我的,那就是WriteableBitmap。WriteableBitmap和GDI+里的Bitmap有些类似,但是局限性比较大一些,不能够直接使用GDI+函数绘制图形,只能绘制诸如控件、图片之类的非几何图形。看到能绘制控件我心里就一下有底了,在GDI+时代,绘制描边效果和阴影一直就是我最擅长的了手段了(偷笑)。

Ok,说干就干。在之前的版本中,弹幕主要是直接使用Textblock添加到页面中然后飞过,如果要使用WriteableBitmap的话,就要把Textblock换成Image,毕竟WriteableBitmap只能依靠Image显示出来。但是Textblock又是不可缺少的一部分,因为绘制文字的时候需要控件图形。

如下所示,声明了一个Textblock,这个是绘制文字的本体,在设置完必要的参数以后执行UpdateLayout可以显式更新控件,让控件以新的设置呈现,这样计算出来的ActualWidth之类的属性才会正确。BarrageFont这里定义的是微软雅黑,这里顺带说明一下,微软雅黑是唯一一个在所有语言版本的Windows Phone里都有的,而且又支持汉字的字体,简体中文系统中自带的等线在英文系统中是没有安装的。而且等线做弹幕的话,线条过细,显示效果并不是非常好。

TextBlock tb = new TextBlock();
tb.Text = ass.m;
tb.FontSize = ass.FontSize * 1.3;
tb.FontFamily = BarrageFont;
tb.UpdateLayout();

定义好字体以后,就要开始绘制了,这里定义描边是1显示单位(在WPF/Silverlight中,所有的Width和Height之类的尺度单位都不是像素,虽然在Windows Phone中,1显示单位确实是1像素,但是在不同的DPI环境下有可能会不相同,这个是由系统定义的),所以画布的长宽都要比Textblock多2个显示单位。

WriteableBitmap bitmap = new WriteableBitmap((int)(tb.ActualWidth + 2), (int)(tb.ActualHeight + 2));

接下来,就要在画布上绘制描边内容,描边的颜色一般都是黑色,但是当弹幕颜色也是黑色的时候,描边最好还是改成白色。这样就要对弹幕的颜色做一个判断:

if ((ass.ForeBrush.Color.R + ass.ForeBrush.Color.B + ass.ForeBrush.Color.G) / 3 >= 32)
{
    tb.Foreground = BarrageShadowBrushBlack;
}
else
{
    tb.Foreground = BarrageShadowBrushWhite;
}

接下来是绘制。绘制的时候不像GDI+那样通过定义Point之类的位置来控制绘图,而是用了Transform类将需要绘制的内容变形、移动来绘制,这一点和XNA上绘制图形是一致的,本质其实是通过矩阵运算然后绘制置图形:

for (int x = 0; x <= 2; x++)
  for (int y = 0; y <= 2; y++)
  {
      if (x == 1 && y == 1)
      continue;
      BarrageTransform.X = x;
      BarrageTransform.Y = y;
      bitmap.Render(tb, BarrageTransform);
  }

然后是绘制弹幕文字:

tb.Foreground = ass.ForeBrush;
BarrageTransform.X = 1;
BarrageTransform.Y = 1;
bitmap.Render(tb, BarrageTransform);

Ok,画完所有的图形之后,再执行一下bitmap.Invalidate();,WriteableBitmap里的内容就生效了,而且可以直接设置为Image控件的Source:

bitmap.Invalidate();
Image image = new Image();
image.Source = bitmap;

最后绘制出来的效果相当不错。因为是使用的Textblock绘制的,而且Windows Phone对这些控件都进行了ClearType处理,所以显示效果不会像Win 32 GDI+那样生硬。

608ace42e654eddeb2c6cc0c66e101ab

Powershell实现更新备份

由于HTC的渣做工,HD7在连接数据网络的时候,只要网络为GPRS或者EDGE,手机就会直接死机、重启,甚至自动恢复出厂设置。在网上查了好多解决方案,都没有办法解决,初步定位应该是HTC的固件有问题。问题已经持续了大半年了,看样子HTC也没打算出解决方案了,所以现在的手机只能将就用着。但是三天两头重置也不是个办法啊,还好微软提供了Windows Phone Engineering Flash Tool,配合离线推送cab包的功能,可以实现无限制备份手机的功能。但是,备份的数据都是保存在C:\Users\Username\AppData\Microsoft\Windows Phone Update\文件夹下,对于我这种经常重装的人来说,放在C盘的内容都是不稳定的,所以需要定期将其备份到其他位置。定期是个比较纠结的方案,因为我不知道定期的间隔该是多少的好,一个月太长,一天又太短,最好能每次备份后都能自动备份就最好了。基本思路确定了,就可以请出Powershell了。使用Powershell判断备份文件夹的最后修改时间,如果发生变动,那么就执行备份操作,否则就退出。Powershell脚本可以设置为每天执行一次。

首先在工作目录下新建一个ps1.config的空文件用于保存最后修改时间,然后新建一个backup.ps1文件,我们的Powershell脚本就写在这里。以下为backup.ps1脚本内容:

$lastWriteConfigStr=Get-Content ps1.config
if($lastWriteConfigStr -eq $null)
{
    [DateTime]$lastWriteConfig="2001-01-1"
}
else
{
    [DateTime]$lastWriteConfig=$lastWriteConfigStr
}
$lastWriteStr=Get-ItemProperty "C:\Users\Zhang Kaidong\AppData\Local\Microsoft\Windows Phone Update\c8a4dfe4 - c99167f2 - 6b87a111 - bf4fff36"
[DateTime]$lastWrite=($lastWriteStr).LastWriteTime
$fileName="backup@"+(Get-Date -Format "yyyyMMdd")+".zip"
if($lastWrite.ToString() -ne $lastWriteConfig.ToString())
{
	& "D:\Program Files\7-zip\7z.exe" a -tzip $fileName "C:\Users\Zhang Kaidong\AppData\Local\Microsoft\Windows Phone Update\c8a4dfe4 - c99167f2 - 6b87a111 - bf4fff36\"
    Set-Content ps1.config -Value $lastWrite
}

Ok,再继续在Windows计划任务中增加一个每日中午12时执行backup.ps1脚本的任务就Ok了。不过要注意的是,添加计划任务时,Action执行的程序应该是“powershell”,在可选的参数里写上“-file 脚本地址”(如:“-file e:\backup\phone\backup.ps1”)。由于在执行的时候需要判断最后更新日期,所以并不是每天都会执行脚本内的备份数据的备份,算是一个折衷的解决方案吧。

豆瓣电台 for Windows Phone 7.5开发笔记 Ex

之前在博客园写过一篇《豆瓣电台 for Windows Phone 7.5开发笔记》,当时草草而谈,并没有太深入研究,现在在这里对之前的开发过程做一个总结。

Windows Phone上的.Net并不是完整的.Net 2、3、3.5、4版本或者其子集,而是一套针对移动设备开发的,类似于.Net Framework for Silverlight,与Windows 8上Metro App使用的WinRT API极其相似的一个开发框架。所以之前有报道说Silverlight已死,死的只是Silverlight这个产品而已,而Silverlight的开发框架将会在Windows Phone和Windows 8中继续发扬光大。另一方面,Sliverlight和WPF一脉相承,抛开执行效率的问题,这本身就是对DirectUI的一次扩展,而DirectUI又是微软最先在XP中使用的独门技术。所以说没有永恒的技术,只有永恒的思想。从Linux到Windows,从C到C++到Java再到C#莫不如此。

扯远了。上面说到.Net的版本问题,就决定了之前在Windows上写的API不能直接拿来用。经过对比,影响最大的部分在网络模块。Windows Phone中大部分的网络操作均只提供异步API,而不提供同步API。这样做的原因是可以达到更好的响应速度,不至于让UI卡死而达到更流畅的用户体验。其实之前在Windows版本上也可以直接用HttpRequest的异步操作,但是当时我的做法是在异步线程上执行HttpRequest的同步操作。效果相同,所以推荐直接用HttpRequest异步方法。

其实入手Windows Phone也快一年了,遇到很多应用最大的部分就是UI不够流畅,而且很明显可以看出来是在进行网络操作的时候整个UI都卡死了。微软的建议是,任何可能超过50毫秒的操作都使用异步执行,麻烦一点,但是带来的体验改善是相当巨大的。

Ok,说完基本的框架,再说说做音乐类应用最重要的AudioPlayerAgent核心。

AudioPlayerAgent和另外两个后台任务类AudioStreamingAgent、ScheduledTaskAgent都是继承自抽象类BackgroundAgent。BackgroundAgent像极了WinProc函数。所有继承自BackgroundAgent的对象都将托管给系统,由系统在某些特定的情况下(如按下下一首、每30分钟),由系统调用托管给系统对象的特定方法,最终执行方法内的操作。为了更好地了解这一过程,可以在Windows Phone App Developer Center上下载的错误堆栈文档。从文档中可以看出,我们写的AudioPlayerAgent其实是被一个名为Microsoft.Phone.BackgroundAgentDispatcher.InvokationTread的方法调用的。微软通过这样的方法让应用程序有一定“后台执行”的能力,但是所有执行的操作又能掌控在系统手上,不至于让应用随意执行,不但不能随意执行,每次执行的时间也是有所限制的。例如上图中的OnUserAction方法,由CallOnUserAction调用后,如果在规定时间内没能执行BackgroundAgent.NotifyComplete方法,系统就会认定这次方法执行失败。而一旦BackgroundAgent.NotifyComplete被调用,本次操作就会全部结束,不管你还有没有正在执行的线程。这里就引出了一个很重要的问题,如果在BackgroundAgent中用HttpRequest网络请求操作使用异步,那么在网络响应结束之前,NotifyComplete被触发,整个BackgroundAgent所在的线程就会被挂起,而网络请求自然也不能成功。解决这个问题的时候我是采用一个简单的线程锁标记来解决的,在网络请求结束以前,设定一个标记将当前线程锁定,直接到网络操作结束再释放。

说到AudioPlayerAgent还有一个不得不说的地方,就是AudioPlayerAgent和主程序之前通信问题。由于在执行的时候AudioPlayerAgent和主程序不属于同一个进程,因此用独立存储的时候经常遇到主程序里的IsolatedStorageSettings更新了,但是AudioPlayerAgent里的IsolatedStorageSettings读取出来还是老的数据(IsolatedStorageFile应该没有这个问题)。而类似像豆瓣电台这样的应用,我需要在主程序和AudioPlayerAgent交换音乐属性(毕竟AudioTrack提供的那几个不够我用的)。在不使用IsolatedStorageFile的情况下,我把额外的交换信息写成了一个ExchangeClass,然后用json序列化放在AudioTrack.Tag中。如果条件许可的情况下,这里还是建议使用IsolatedStorageFile。

关于多语言支持,一开始是没有考虑到这层的,一旦使用了以后就会发现微软在设计框架时是多么精巧。Windows Phone里的多语言支持和XAML的数据绑定是相通的,直接设定一个关键字,然后针对这个关键字编写不同语言版本的资源文件,再然后编译一下就搞定了。唯一需要提的一点是应用在Applications里名称的全球化支持,需要一个标准的Win32 DLL作为资源文件(MSDN),然后根据不同的语言编码(16进制)写不同的资源文件,放在xap根目录中。Windows会根据当前系统语言自动寻找对应的资源文件然后显示,从这点也可以说明微软自己在开发Windows Phone的时候应该更主要用的是C++。对于官方的应用,Native C++是最有可能的选择了。

关于Silverlight Toolkit for Windows Phone,我只能说这是个好工具,但是使用的时候要适可而止。我一直不明白微软为什么不直接在系统里支持Toolkit里的功能,而要单独再创建一个开源项目来实现它。臃肿就不说了,效率也是Toolkit的一大问题。类似像TiltEffect.IsTiltEnabled的特性,如果不是特殊必要,应该考虑舍弃。因为Toolkit里的TileEffect的效率实在是太差了,如果页面上图片太多,启用TileEffect就会导致拖动页面的时候经常性的卡死,ListBox尤其如此,这也是为什么很多包括ListBox应用拖动列表时卡顿明显的原因。

在开发过程中大致遇到的问题就这些了。Windows Phone入门实在是太低太低了,下载安装个SDK,新建个项目随便拖拖就能出个应用,但是想真正做一个精品应用,UI美观大方,运行流畅,对开发者的要求却又过高。另外再吐槽下微软的产品,博大而又精深。

Windows Phone 7中带题头的TextBox和PasswordBox

之前在做豆瓣电台的时候想实现一个登录界面,起初是把TextBlock和TextBox放在一起(效果如下),但是这样实现出来的效果既浪费空间,又不美观。如下图:

202614M25-0

因为之前写WPF对自定义控件有一些基础,所以就对TextBox和Password的Template做了些手脚,让题头融合到控件里。这样既美观又节省空间,同时还可以保证不会推动系统控件风格和功能,如下图:

2026142Z9-1

实现上面的效果非常简单,只需要在Blend中创建一个新的TextBox,然后右键-Edit Template-Edit a Copy。在弹出的对话框中的Name设定一个名字,如“LabelTextBox”,然后在Define in中选择Application以在整个程序范围内都可以使用它。

操作完这些以后Blend会自动在App.xaml里创建一个新的Style资源,我们在里面搜索一下ContentControl,这就是TextBox中显示文字的元素了。剩下的就是对这个元素进行改造,如调整位置,添加新元素等等。下面是我写的TextBox和PasswordBox的资源:

<ControlTemplate x:Key="PhoneDisabledTextBoxTemplate" TargetType="TextBox">
            <ContentControl x:Name="ContentElement" BorderThickness="0" HorizontalContentAlignment="Stretch" Margin="{StaticResource PhoneTextBoxInnerMargin}" Padding="{TemplateBinding Padding}" VerticalContentAlignment="Stretch"/>
        </ControlTemplate>
        <Style x:Key="LabelTextBox" TargetType="TextBox">
            <Setter Property="FontFamily" Value="{StaticResource PhoneFontFamilyNormal}"/>
            <Setter Property="FontSize" Value="{StaticResource PhoneFontSizeMediumLarge}"/>
            <Setter Property="Background" Value="{StaticResource PhoneTextBoxBrush}"/>
            <Setter Property="Foreground" Value="{StaticResource PhoneTextBoxForegroundBrush}"/>
            <Setter Property="BorderBrush" Value="{StaticResource PhoneTextBoxBrush}"/>
            <Setter Property="SelectionBackground" Value="{StaticResource PhoneAccentBrush}"/>
            <Setter Property="SelectionForeground" Value="{StaticResource PhoneTextBoxSelectionForegroundBrush}"/>
            <Setter Property="BorderThickness" Value="{StaticResource PhoneBorderThickness}"/>
            <Setter Property="Padding" Value="2"/>
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="TextBox">
                        <Grid Background="Transparent">
                            <VisualStateManager.VisualStateGroups>
                                <VisualStateGroup x:Name="CommonStates">
                                    <VisualState x:Name="Normal"/>
                                    <VisualState x:Name="MouseOver"/>
                                    <VisualState x:Name="Disabled">
                                        <Storyboard>
                                            <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Visibility" Storyboard.TargetName="EnabledBorder">
                                                <DiscreteObjectKeyFrame KeyTime="0">
                                                    <DiscreteObjectKeyFrame.Value>
                                                        <Visibility>Collapsed</Visibility>
                                                    </DiscreteObjectKeyFrame.Value>
                                                </DiscreteObjectKeyFrame>
                                            </ObjectAnimationUsingKeyFrames>
                                            <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Visibility" Storyboard.TargetName="DisabledOrReadonlyBorder">
                                                <DiscreteObjectKeyFrame KeyTime="0">
                                                    <DiscreteObjectKeyFrame.Value>
                                                        <Visibility>Visible</Visibility>
                                                    </DiscreteObjectKeyFrame.Value>
                                                </DiscreteObjectKeyFrame>
                                            </ObjectAnimationUsingKeyFrames>
                                        </Storyboard>
                                    </VisualState>
                                    <VisualState x:Name="ReadOnly">
                                        <Storyboard>
                                            <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Visibility" Storyboard.TargetName="EnabledBorder">
                                                <DiscreteObjectKeyFrame KeyTime="0">
                                                    <DiscreteObjectKeyFrame.Value>
                                                        <Visibility>Collapsed</Visibility>
                                                    </DiscreteObjectKeyFrame.Value>
                                                </DiscreteObjectKeyFrame>
                                            </ObjectAnimationUsingKeyFrames>
                                            <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Visibility" Storyboard.TargetName="DisabledOrReadonlyBorder">
                                                <DiscreteObjectKeyFrame KeyTime="0">
                                                    <DiscreteObjectKeyFrame.Value>
                                                        <Visibility>Visible</Visibility>
                                                    </DiscreteObjectKeyFrame.Value>
                                                </DiscreteObjectKeyFrame>
                                            </ObjectAnimationUsingKeyFrames>
                                            <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Background" Storyboard.TargetName="DisabledOrReadonlyBorder">
                                                <DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource PhoneTextBoxBrush}"/>
                                            </ObjectAnimationUsingKeyFrames>
                                            <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="BorderBrush" Storyboard.TargetName="DisabledOrReadonlyBorder">
                                                <DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource PhoneTextBoxBrush}"/>
                                            </ObjectAnimationUsingKeyFrames>
                                            <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Foreground" Storyboard.TargetName="DisabledOrReadonlyContent">
                                                <DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource PhoneTextBoxReadOnlyBrush}"/>
                                            </ObjectAnimationUsingKeyFrames>
                                        </Storyboard>
                                    </VisualState>
                                </VisualStateGroup>
                                <VisualStateGroup x:Name="FocusStates">
                                    <VisualState x:Name="Focused">
                                        <Storyboard>
                                            <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Background" Storyboard.TargetName="EnabledBorder">
                                                <DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource PhoneTextBoxEditBackgroundBrush}"/>
                                            </ObjectAnimationUsingKeyFrames>
                                            <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="BorderBrush" Storyboard.TargetName="EnabledBorder">
                                                <DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource PhoneTextBoxEditBorderBrush}"/>
                                            </ObjectAnimationUsingKeyFrames>
                                        </Storyboard>
                                    </VisualState>
                                    <VisualState x:Name="Unfocused"/>
                                </VisualStateGroup>
                            </VisualStateManager.VisualStateGroups>
                            <Border x:Name="EnabledBorder" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" Margin="{StaticResource PhoneTouchTargetOverhang}">
                                <Grid>
                                    <Grid.ColumnDefinitions>
                                        <ColumnDefinition Width="Auto" />
                                        <ColumnDefinition Width="*" />
                                    </Grid.ColumnDefinitions>
                                    <TextBlock Grid.Column="0" Margin="10, 0" Text="{TemplateBinding Tag}" VerticalAlignment="Center" />
                                    <ContentControl Grid.Column="1" x:Name="ContentElement" BorderThickness="0" HorizontalContentAlignment="Stretch" Margin="1,2,1,2" Padding="{TemplateBinding Padding}" VerticalContentAlignment="Stretch" VerticalAlignment="Center"/>
                                </Grid>
                            </Border>
                            <Border x:Name="DisabledOrReadonlyBorder" BorderBrush="{StaticResource PhoneDisabledBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="Transparent" Margin="{StaticResource PhoneTouchTargetOverhang}" Visibility="Collapsed">
                                <Grid>
                                    <Grid.ColumnDefinitions>
                                        <ColumnDefinition Width="Auto" />
                                        <ColumnDefinition Width="*" />
                                    </Grid.ColumnDefinitions>
                                    <TextBlock Grid.Column="0" Margin="10, 0" Text="{TemplateBinding Tag}" VerticalAlignment="Center" Foreground="{StaticResource PhoneDisabledBrush}" FontWeight="{TemplateBinding FontWeight}" FontStyle="{TemplateBinding FontStyle}" FontSize="{TemplateBinding FontSize}" FontFamily="{TemplateBinding FontFamily}" />
                                    <TextBox Grid.Column="1" x:Name="DisabledOrReadonlyContent" Background="Transparent" Foreground="{StaticResource PhoneDisabledBrush}" FontWeight="{TemplateBinding FontWeight}" FontStyle="{TemplateBinding FontStyle}" FontSize="{TemplateBinding FontSize}" FontFamily="{TemplateBinding FontFamily}" IsReadOnly="True" SelectionForeground="{TemplateBinding SelectionForeground}" SelectionBackground="{TemplateBinding SelectionBackground}" TextAlignment="{TemplateBinding TextAlignment}" TextWrapping="{TemplateBinding TextWrapping}" Text="{TemplateBinding Text}" Template="{StaticResource PhoneDisabledTextBoxTemplate}"/>
                                </Grid>
                            </Border>
                        </Grid>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
        <ControlTemplate x:Key="PhoneDisabledPasswordBoxTemplate" TargetType="PasswordBox">
            <Border x:Name="ContentElement" BorderThickness="0" Margin="{StaticResource PhonePasswordBoxInnerMargin}" Padding="{TemplateBinding Padding}"/>
        </ControlTemplate>
        <Style x:Key="LabelPassword" TargetType="PasswordBox">
            <Setter Property="FontFamily" Value="{StaticResource PhoneFontFamilyNormal}"/>
            <Setter Property="FontSize" Value="{StaticResource PhoneFontSizeMediumLarge}"/>
            <Setter Property="Background" Value="{StaticResource PhoneTextBoxBrush}"/>
            <Setter Property="Foreground" Value="{StaticResource PhoneTextBoxForegroundBrush}"/>
            <Setter Property="BorderBrush" Value="{StaticResource PhoneTextBoxBrush}"/>
            <Setter Property="BorderThickness" Value="{StaticResource PhoneBorderThickness}"/>
            <Setter Property="SelectionBackground" Value="{StaticResource PhoneAccentBrush}"/>
            <Setter Property="SelectionForeground" Value="{StaticResource PhoneContrastBackgroundBrush}"/>
            <Setter Property="Padding" Value="2"/>
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="PasswordBox">
                        <Grid Background="Transparent">
                            <VisualStateManager.VisualStateGroups>
                                <VisualStateGroup x:Name="CommonStates">
                                    <VisualState x:Name="Normal"/>
                                    <VisualState x:Name="MouseOver"/>
                                    <VisualState x:Name="Disabled">
                                        <Storyboard>
                                            <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Visibility" Storyboard.TargetName="EnabledBorder">
                                                <DiscreteObjectKeyFrame KeyTime="0">
                                                    <DiscreteObjectKeyFrame.Value>
                                                        <Visibility>Collapsed</Visibility>
                                                    </DiscreteObjectKeyFrame.Value>
                                                </DiscreteObjectKeyFrame>
                                            </ObjectAnimationUsingKeyFrames>
                                            <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Visibility" Storyboard.TargetName="DisabledBorder">
                                                <DiscreteObjectKeyFrame KeyTime="0">
                                                    <DiscreteObjectKeyFrame.Value>
                                                        <Visibility>Visible</Visibility>
                                                    </DiscreteObjectKeyFrame.Value>
                                                </DiscreteObjectKeyFrame>
                                            </ObjectAnimationUsingKeyFrames>
                                        </Storyboard>
                                    </VisualState>
                                </VisualStateGroup>
                                <VisualStateGroup x:Name="FocusStates">
                                    <VisualState x:Name="Focused">
                                        <Storyboard>
                                            <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Background" Storyboard.TargetName="EnabledBorder">
                                                <DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource PhoneTextBoxEditBackgroundBrush}"/>
                                            </ObjectAnimationUsingKeyFrames>
                                            <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="BorderBrush" Storyboard.TargetName="EnabledBorder">
                                                <DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource PhoneTextBoxEditBorderBrush}"/>
                                            </ObjectAnimationUsingKeyFrames>
                                        </Storyboard>
                                    </VisualState>
                                    <VisualState x:Name="Unfocused"/>
                                </VisualStateGroup>
                            </VisualStateManager.VisualStateGroups>
                            <Border x:Name="EnabledBorder" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" Margin="{StaticResource PhoneTouchTargetOverhang}">
                                <Grid>
                                    <Grid.ColumnDefinitions>
                                        <ColumnDefinition Width="Auto" />
                                        <ColumnDefinition Width="*" />
                                    </Grid.ColumnDefinitions>
                                    <TextBlock Grid.Column="0" Margin="10, 0" Text="{TemplateBinding Tag}" VerticalAlignment="Center" />
                                    <Border Grid.Column="1" x:Name="ContentElement" BorderThickness="0" Margin="{StaticResource PhonePasswordBoxInnerMargin}" Padding="{TemplateBinding Padding}"/>
                                </Grid>
                            </Border>
                            <Border x:Name="DisabledBorder" BorderBrush="{StaticResource PhoneDisabledBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="Transparent" Margin="{StaticResource PhoneTouchTargetOverhang}" Visibility="Collapsed">
                                <Grid>
                                    <Grid.ColumnDefinitions>
                                        <ColumnDefinition Width="Auto" />
                                        <ColumnDefinition Width="*" />
                                    </Grid.ColumnDefinitions>
                                    <TextBlock Grid.Column="0" Margin="10, 0" Text="{TemplateBinding Tag}" VerticalAlignment="Center" Foreground="{StaticResource PhoneDisabledBrush}" FontWeight="{TemplateBinding FontWeight}" FontStyle="{TemplateBinding FontStyle}" FontSize="{TemplateBinding FontSize}" FontFamily="{TemplateBinding FontFamily}" />
                                    <PasswordBox Grid.Column="1" x:Name="DisabledContent" Background="Transparent" Foreground="{StaticResource PhoneDisabledBrush}" Password="{TemplateBinding Password}" PasswordChar="{TemplateBinding PasswordChar}" Template="{StaticResource PhoneDisabledPasswordBoxTemplate}"/>
                                </Grid>
                            </Border>
                        </Grid>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>

Ok,样式设定完毕,要使用的时候就非常简单了,在项目里的任何一个TextBox或者PasswordBox中像这样使用它就可以了:

<TextBox Tag="帐号:" Style="{StaticResource LabelTextBox}" Name="Username" />
<PasswordBox Tag="密码:" Style="{StaticResource LabelPassword}" Name="Password" />

大功告成!其中Tag是控件前面题头里的文字,而TextBox和PasswordBox的功能不会受到任何影响~