WPF/Silverlight编程:理解应用程序编程模型

概述

本文介绍WPF/Silverlight技术为开发人员提供了怎样的编程技术,为了完成我们实际开发中的需求,WPF/Silverlight提供了什么样的新方法。

首先分析它们产生前的编程技术,得出需要改进的地方,这些问题也是我们实际开发过程中会经常遇到的“痒痒”;然后介绍 WPF/Silverlight的设计思路,说明它提供了怎样的解决方案来解决那些老技术所不能解决的问题;再后,解释WPF/Silverlight所 发明的一些新概念,是如何概括、总结新的解决方案的,这些新概念就是它提供的编程模型;最后,具体说说这些编程模型。

“自图形窗口系统产生以来,到WPF/Silverlight技术的产生,GUI应用程序(包括嵌入式系统的MMI,下同)的编程方式和技术,已经至少有15年之久没有什么根本变化了!”----仅以此句开篇,说明WPF/Silverlight的革命性意义。

文中纰漏,敬请不吝赐教,一同讨论。

XAML的来由和作用、新的开发方式

下图是我总结的GUI应用程序编程框架需要解决的问题。

GUI-programming-outline

用户操作软件获得视觉体验和功能体验。编程框架需要处理的其实就是三个方面:用户界面本身、功能数据本身和两者间的关联。用户都是首先接触到用户界面,所以我们也是从用户界面入手分析。用户界面的处理可以抽象为"LIFE(Layout, Input, Focus, Event)"。 其中Layout由Control(widget)组成,Control接受用户的input,用户在某个时刻需要Focus在某个Control上,用 户的操作被看成Event,驱动用户界面的变化,用户的Focus也在变化。我们先看看那些“史前产品”是怎么实现的。

WPF/Silverlight产生前的Windows GUI应用程序编程模型

这里说的GUI框架,是这些:桌面领域的Windows Forms,GTK,QT,Java swing等;手持领域的BREW UI, QT Embedded,Android等。

1,用户界面Layout的编写有两种方式:在代码中构造;使用描述性语言描述(基本都是XML的方式,常被称为界面资源文件) + 相应的界面资源获取机制以便在代码中获得Layout上control的引用。在界面资源文件中能定义Layout结构以及挂接Layout中 control的事件处理函数,无法处理control关联的数据对象,这是要在代码中才能处理的。

2,用户界面control的定制方式:从已有control继承,封装一些基本control,然后重新实现某些重绘函数,在其中要使用基本的绘图API,“精巧”而又“小心翼翼”的实现一些特殊界面效果。

3,用户界面的动态显示效果:与control的定制类似,自己写代码实现。

4,使用callback机制处理用户操作产生的event,自己实现event的处理函数(Listener),再使用特定的API注册到GUI框架中,当用户操作时,由GUI框架反过来调用开发者自己实现的event处理函数。

尤其是对control的定制方面由为困难,因为对于一个control来说,不仅要处理它所封装的基本control的现实,还要处 理接受用户focus时的事件处理,这个新control的事件和它封装的基本control们的event关系又怎么处理,整个过程需要仔细设计和处 理,当然也就难于维护。

造成control定制的困难就在于这些GUI框架看待control的方式,它们只是把layout看成是control的组成,没有对control做进一步的结构分析,特点是:

1,每个control都是一个不可再分的整体。其显示样式(形状、颜色、使用的字体)和其上的数据(字符串、图片)是紧耦合的

2,每个control的底层现实是基于最基本的绘图API,这些绘图API不是单独的对象,需要图形学方面的专业知识才能较好使用,即并不是开发人员友好的,而是设计人员友好的。

3,最基本的绘图API也是bitmap方式的,这给GUI应用程序适应不同尺寸和分辨率的设备造成根本影响。也为特殊的界面效果(透明、阴影等)造成阻碍。

4,对显示资源的复用性不高。显示资源包括颜色、字体、画刷、字符串、图形等。这样会影响性能。

而现在对用户界面的要求越来越高,更自然的呈现方式是方向,这包括新的界面样式的特点,动态转换界面等等,所以在技术方面,我们需要更新认识,就像从面向过程转移到面向对象一样。

新观念

1,用户界面由Layout组成,Layout由control组成,形成“父- 子 - 兄弟”的层次结构关系。这一点传统的图形框架经过改进,已经具备。

2,基本control也有自己的layout,其中可以组合任意其它control。这是新东西,结合第一点,整个用户界面的结构将是允许无限扩展的。

3,control的显示和内容彻底分离,这样可以单独描述其显示样式和内容,从而为control定制提供更方便的支持。

4,用户界面的定义优先使用描述性语言定义。显示资源可以复用。传统的图形框架入Android, BREW也已经具备,但还不够。尤其是根据第2点的要求。

5,用户界面的动态效果要被抽象出来,封装成单独的对象使用并由图形框架管理。这是新东西,界面动画要被看成"一等公民"。

6,用户界面定义文件能够和代码互操作,能够相互引用对象,好办法就是由工具自动从用户界面定义文件生成代码,并允许开发者在不用修改这个代码的基础上,扩展和添加新代码。这也是新东西,并且要求编程工具的集成。

WPF/Silverlight产生前的Windows GUI应用程序开发流程

跳出技术方案的细节,我们再来看看软件工程方面。

在传统GUI应用程序开发的工作流程中,往往需要美工设计人员提供完整的bitmap资源,开发人员依赖美工的工作,因为没有美工制作的bitmap资源,应用程序的界面几乎不能进行开发。如下图所示:

GUI-development-old-roles

这样的协作关系的产生,多半是由于美工设计人员不懂专业的编程语言,可是这样的方式显然导致了开发周期的增加,在现在高交付频率的时代,也有些跟不上时代的要求了。我们希望这两个角色能更多的并行工作,如下图所示:

GUI-development-new-roles

这当然是微软提出的解决方案啦(参考:WM7 App 开发流程参考), 不过它确实很有参考意义。暂时不考虑上图中的XAML。也就是说,想要美工设计人员更多的和开发人员并行工作,就必须提供一种技术上的支持,大大降低美工 设计人员直接接触专业编程语言的需要,那么更接近人类自然语言的描述性标志语言就是目前最好的选择了。更进一步来看,就算是描述性标志语言,美工设计人员 仍然不算熟悉,那就提供所熟悉的设计工具来达到这个目的,然后再由工具自动生成描述性标志语言,然后再和代码进行交互。

微软就设计了一个XAML来达到这样的目的,并提供了Expression Studio工具集来支持这一新的开发流方式。更多内容,参考这里:How XAML Transforms the Collaboration Between Designers and developers in WPF

XAML的产生

这样我们在考虑一个新的图形编程框架时,不仅考虑它如何解决旧系统不适应新的需求而产生的技术缺陷,而且更宏观的在软件开发过程上改进旧的方式,这两个方面归结到一点上,就是“用描述性的方式进行软件开发的需要”。传统的图形应用程序编程,可以被称为“用编译性的方式进行软件开发”(这个说法是我个人发明的,仅用来对比说明之用)。说到这里,请大家结合已有的开发经验好好体会它的含义,以及对我们将来的开发方式的革新意义

用描述性的方式,可以:

1,更紧密地结合美工设计人员和开发人员的工作,将UI设计无缝的转换到UI实现上来:美工->设计工具->自动生成描述性语言文本->编程语言平台自动解释->自动生成对应的编程语言代码->开发人员进行扩展、添加,专注于业务功能UI界面的设计和实现完全和业务功能的实现更好的分离了。

2,上一点是分离软件的UI部分和业务功能部分,这里对UI部分再做进一步的分离:UI视觉呈现和UI数据内容的分离。后者包括处理用户输入和需要输出到UI上的数据。这样,经常多变的UI界面将被更加独立构造。

XAML之前,几乎没有支持1,对2的支持也十分有限。因为在之前的编程框架中,描述性方式并不是以一等的方式在使用,这是因为它们还是以计算机系统如何实现,而不是从人(包括参与软件开发的所有人)的使用角度来考虑的。

多说一点题外话。确立描述性方式为主导的思维,可以为软件开发带来新局面,这东西不是一蹴而就的,也是慢慢发展而来的。采用描述性的方 式,软件开发建模技术将充分发展起来,软件模型是描述性的,甚至可以被公司的市场人员直接使用;用户也能直接使用和编写,然后使用工具自动将模型翻译成为 代码,这会让纯粹的代码编写工作量变得很小,而软件的开发速度将进一步提高。有兴趣者可以参考下OMG的MDA架构。

OK,回到主题。现在描述性标志语言有很多,比如HTML,XUL,SVG,WordML,为什么XAML是基于XML的呢?因为相对 于其它的几种界面描述性语言,XML-Based最适合描述内容,而不是格式,这是被非开发人员阅读的重要根据,同时它也非常适合描述Layout的“父 - 子 -兄弟”结构关系。

根据《pro WPF》上描述,XAML提供的功能如下:

1,Wiring up event handlers.装配事件处理器。

2,Defining resources.定义资源。

3,Defining control templates.定义控件模板。

4,Writing data binding expressions.编写数据绑定表达式。

5,Defining animations.定义动画。

就如我当初看到这些内容以及MSDN上对XAML的介绍的时候,怎样理解其背后的设计来源和思路呢?它们只是说明了其功能,对来源只字未提,那又如何懂得去应用呢?通过网上众多技术资料和开发者的心得总结,就会得出来,呵呵,用中国的话说,这叫“道”。也许最好的学习方法就是不仅仅去看别人写的东西,更要总结出来自己的东西,这才能说真正掌握吧。

这里不再介绍具体XAML是如何编写的,参考:WPF/Silverlight编程:控件定制中的内容。

新的开发方式

经过从“以编译性的方式”到“以描述性的方式”的开发思路的转变,使用WPF/Silverlight(以及类似的编程框架)进行应用程序开发:

1,对于项目组织管理来说,注重给美工设计人员多一些相关培训。当然前提是一个组织决定要使用MS给出的开发流方式。

2,对于美工设计人员来说,需要学习使用设计工具。

3,我是开发人员,所以重点说说这个。在WPF/Silverlight中开发软件,首先不是去写一个class,而是去写XAML文件。虽然微软的说法是不提倡手工直接编写XAML文件,应该使用Expression Studio工具来生成,然后再手工调整,但我的看法是开发人员必须学习XAML的结构,以及掌握直接使用编辑器编写的能力。由XAML工具生成的代码,叫“XAML的隐藏代码”,开发人员绝对不要手工去修改它们,借由c#提供的部分类扩展方法,开发人员可以自由的扩展它们,以及创建新的类来处理业务功能。这里的软件设计重点在于处理业务功能与UI整体部分的结构,还有业务功能和UI数据内容之间的结构。

编程模型的构成

到目前为止,大家应该了解了WPF/Silverlight的编程模型的来源,下面就是具体的内容。但WPF和Silverlight还是有区别的。这在另一地方介绍:开发者视角:WPF与Silverlight的联系和区别,这里我们还是讲相同的基础部分。根据第2节开头的“GUI应用程序编程框架需要解决的问题”的图所示,WPF/Silverlight提供的解决方案如下图:

WPF.Silverlight.Model

针对问题域的三个方面,WPF/Silverlight都有对应的部分来处理,它的编程模型也主要是控件模型动画模型,以及起到基本支持作用的依赖项属性系统路由事件系统,还有一些附带的资源管理部分。

通过上图,我们可以了解WPF/Silverlight提供了哪些东西,我们暂时不要深入到它们是怎么工作的,这是更加深入地内容(参考:WPF/Silverlight编程:依赖项属性系统和路由事件系统分析)。现在,我们来看看Silverlight中UI相关的类层次组织(WPF的不同,参考开发者视角:WPF与Silverlight的联系和区别),这些类对应了上图中的概念,体现了微软的设计思路。

Silverlight3.UI.Class.Tree1

上图中黑体的部分是和控件定制有关的重点。所有WPF/Silverlight对象都是从DependencyOjbect继承的,这 样才能具有依赖项属型,依赖项属型是另一个类DependencyProperty,它们都继承自顶级类Object。样式由类Style定义。动画模型 由一组类:VisualState、VisualStateGroup、VisualStateManager、VisualTransition、 Animation.TimeLine以及子类(其中有StoryBoard;类DoubleAnimation最常用,用来实现StoryBoard) 定义。另外还有一些表示资源的类Brush,Geometry。类GeneralTransform的子类用来实现转换的处理,也应该是动画模型的一部 分,与此类似的还有类Effects.Effect,用来实现模糊和阴影等效果。类FrameworkTemplate的子类有控件模板 (ControlTemplate)和数据模板(DataTemplate)。

类UIElement和子类FrameworkElement是布局和控件基类,如下图:

Silverlight3.UI.Class.Tree2

上图显示了Silverlight 3中提供的所有内置控件。其实,在WPF/Silverlight中,控件应该为UI Element,把其中可以获得焦点、能接受按键/鼠标操作的Element叫做Control。所以在上图中,Border、Image、TextBlock、Shape、Panel等不属于类Control下。而对于Control(这里指可以获得用户输入的Elemet)被分为两类:ContentControlItemsControl。前者表示这个Control只能有一个成员,后者则表示有一组成员。所以,类ContentControl下有Frame、Lable、Button、ChildWindow等,而像TreeView、ListBox之类的就放在类ItemsControl中。

在实际开发中,那些不接受用户操作的控件,往往用来组织布局,和修饰那些能接受用户操作的控件,而且往往不会在代码中被引用到。这些可以被称为结构控件;那些接受用户操作的控件往往被称为功能控件,也是经常会被用到的。

将类Control下分成ContentControl和ItemsControl,就是因为其中具体的control的控件模型是不 同的,具体的说,就是控件内容模型是不同的。这样,在以这些控件为模板定制控件时,写法也是不同的。类ContentPresenter是为了显示 ContentControl的内容,而类ItemsPresenter是为了显示ItemsControl的内容。更多控件模型的内容,参考:WPF/Silverlight编程:控件定制

核心编程模型注解

上一节提到了WPF/Silverlight的编程模型构成,能为开发者所直接使用的,可以归结为控件模型和动画模型,而像数据的处理模型,可以依附在控件模型和动画模型上来看。

可能您会说WPF/Silverlight这么复杂,怎么就只有这两个东西呢?就如同书要看薄一样,要知道,WPF/Silverlight就是为了解决界面显示方面的需求,业务功能它根本也不相关,就连界面相关的数据也要围绕UI显示来考虑。

但是、表面的简洁掩盖了下面的复杂性,拨开这些复杂,找到隐藏在最下面的宝石,这是我们学习的必经之路。这个宝石就是WPF/Silverglith的依赖项属型系统和路由事件系统。控件模型和动画模型的详细阐述,还是参考:WPF/Silverlight编程:控件定制

解释一下它们,需要从实例入手,请看下面的XAML代码:

<Button FontSize="20">Test</Button>

上面的代码,就是给个Button设置了属性字体大小是20。再看下面的代码:

<Button FontSize="20">
     <TextBlock>Test</TextBlock>
</Button>

这次在Button里嵌入一个TextBlock,将Test字符串内容显示在其上,问一下这个Test的字体大小是多少呢?假如代码如下面所写呢?

<Button>
     <TextBlock FontSize="20">Test</TextBlock>
</Button>

继续来,在这个Button里放入两行文本,注意这里要先放入StackPanel,因为Button是一种ContentControl,只能有一个成员:

<Button>
     <StackPanel>
          <TextBlock FontSize="20">Test1</TextBlock>
          <TextBlock FontSize="20">Test2</TextBlock>
     </StackPanel>
</Button>

这样重复设置FontSize属性,是不是很麻烦,所以我想这样,同时再放入一行文本:

<Button FontSize="20">
     <StackPanel>
          <TextBlock>Test1</TextBlock>
          <TextBlock>Test2</TextBlock>
          <TextBlock FontSize="30">Test3</TextBlock>
     </StackPanel>
</Button>

这样子,Test1和Test2的字体大小都是20,而Test3的字体大小是30了。上面的例子体现了“依赖项”属性的概念。子控件的属性“依赖”父控件的同名属性。TextBlock的字体大小会自然继承其父控件的属性值,这是一个很自然的要求,考虑一下Android一类的界面描述语言,有无此功能呢?

换句话说,对象Button和对象TextBlock的FontSize属性发生了关联,这对于CLR中的C#类的传统属性功能,是超 越性的。这种关系涉及到WPF/Silverlight如何实现这种关联的问题,否则的话,在解析XAML文件的时候,如何正确设置Button中任何一 个子成员的对应属性呢?

依赖项属性系统

对于每个WPF/Silverlight中的控件对象来说,除了C#语言规定的属性来说,没有特殊的叫做“依赖项属型”的语法。比如对于 Button来说,它只有一个FontSize属性,这个属性是否是“依赖项”属性,取决于它有没有将FontSize注册给类 DependencyProperty。而且,能够在代码中注册自己的属性给DependencyProperty的控件,都必须从 DependencyObject继承。这里不再具体说明,参考:WPF/Silverlight编程:依赖项属性系统和路由事件系统分析。

这些控件有很多属性,其中有很大一部分是“依赖项”属性。非依赖项属型几乎都不能在XAML文件中被使用。

依赖项属型除了像上面的代码示例的那样:继承属性值,还有其它的变种,比如附加属性。说某个控件的附加属性,也不是说它有一个新的属性语法类型,只是一种称呼,附加属性也是一种依赖项属性。

附加属性例子如下:

<Canvas>
     <Button Name="button1" Canvas.Left="100" >button</Button> 
     <!-- Canvas的Left属性就是一个附加属性,当然也是一个依赖项属型。它被设置到Button对象身上,Button中并没有设置Left属性,-->
     <!-- 所以形象地把Canvas的Left属性叫附加属性。另外,附加属性的值会被保存在被附加对象内,而不是定义它的对象上,这里当然就是 -->
     <!-- Button对象保存了Canvas的Left对象值,呵呵,有点弯弯绕需要进一步理解。这里就不说了。-->
</Canvas>

依赖项属型的功能如下:

1,使用Resources(资源)

<Button Background="{DynamicResource MyBrush}"/> <!-- 因为MyBrush被Button使用,所以Background依赖了某个对象,就被称为依赖项属型 -->

2,Data binding(数据绑定)

<Button Content="{Binding XPath=Team/@TeamName}"/> <!-- Button的Content属性依赖别人了,所以它是依赖项属型 -->

3,Styles(样式)

<Style x:Key="GreenButtonStyle">
<Setter Property="Control.Background" Value="Green"/>
</Style>   <Button Style="{StaticResource GreenButtonStyle}"> <!-- 首先Style是个依赖项属型,而且Button使用了Style对象中的Background属性,所以是依赖项属型 -->
</Button>

4,Animations(动画)

<Button>I am animated
  <Button.Background>
    <SolidColorBrush x:Name="AnimBrush"/>
  </Button.Background>
  <Button.Triggers>
    <EventTrigger RoutedEvent="Button.Loaded">
      <BeginStoryboard>
        <Storyboard>
          <ColorAnimation
            Storyboard.TargetName="AnimBrush"  <!-- Storyboard对象发生在Button对象身上,所以Coloer属性是个依赖项属性,才可以有作用-->
            Storyboard.TargetProperty="Color"
            From="Red" To="Green" Duration="0:0:5" 
            AutoReverse="True" RepeatBehavior="Forever" />
        </Storyboard>
      </BeginStoryboard>
    </EventTrigger>
  </Button.Triggers>
</Button>

5,Metadata overrides(元数据覆盖)

<!--高级话题,暂不涉及 -->

6,Property value inheritance(值继承)

<!-- 就像刚才的例子 -->

7,WPF Designer integration(WPF设计集成)

<!-- 意思是自定义的控件放到Visual Studio上,会自动放到控件面板中 -->

路由事件系统

与依赖项属型类似,路由事件也表达了一种事件处理上依赖关系,但这种依赖关系,更多的体现在事件在对象链上的传递,表达出来的概念是这样的,在 对象链上传递的事件有最先的源头,依次传递过去后,链路上的对象可以选择处理它,也可以丢给下一个对象处理,都没有处理的话,就由系统处理。见下面的代 码:

<StackPanel Button.Click="button_Click"> <!-- 和附加属性类似,这里的Button.Click叫附加事件,也是一种路由事件 -->
     <Button>Ok</Button>
     <Button>Close</Button>
</StackPanel>

可以在XAML中这么写,不管用点击了Ok按钮,Close按钮,button_Click事件处理函数都可以被触发。

路由事件分成两种,一种从子控件传递到父控件的,叫冒泡路由(Bubbling Events);另一种从父控件传递到子控件的,叫隧道路由(Tunneling Events)。WPF支持两者,Silverlight只支持前者。

与依赖项对象类似,路由事件本质上不是新的C#语法,所有继承自DependencyObject的对象可以向 EventManager注册它的事件为路由事件,路由事件消息体RoutedEventArgs中含有附加的信息,以便Event在路由时,可以被路径 上的对象处理或者忽略掉。

下一步

进入WPF/Silverlight编程:控件定制,通过实例了解XAML,以及控件模型、动画模型

posted @ 2013-01-04 14:23  Leo L.Cao  阅读(548)  评论(2编辑  收藏  举报