WPF 基础到企业应用系列4——WPF千年轮回
1.开篇前言
首先很高兴这个系列能得到大家的关注和支持,基于对大家负责和对自己负责的态度,我会不断努力写好这个系列,分享自己的微薄技术和经验,希望在帮助别人的同时也不断提升自己。由于这篇文章很多(现已拆分成2篇,今天这篇只是其中之一),一共花了几个个晚上的休息时间才完成,所以读者花的时间长了一些,也希望大家能够见谅,这个系列以后会每周发三到四篇左右(主要是写一篇差不多要花几晚上,感觉思维比较发散),除了讲WPF技术本身之外,也会讲一些项目具体开发,所以敬请关注。
本篇文章取名为WPF千年轮回只因为两个原因:
- WPF和当年Win32、WinForm等的到来颇为相似,只是在功能和体验上上进行了提高,所以这是微软产品上的一个轮回;
- WPF的学习过程和其他技术一样,譬如ASP.NET,我们在学习的时候会先要了解Asp.Net构架(Http请求处理流程)、Pipeline、HttpHandler 和 HttpModule 等内容,这和WPF的Application生命周期相对应,再如WPF的Window生命周期可以和ASP.NET的页面生命周期相对应等。当然你也可以拿WinForm或者其他技术来举例,这里这是阐述观点。
在前三篇文章中我们对WPF有了一个比较全面的认识,并且也通过一个基本的例子对比了WPF和之前的WinForm程序的区别和联系。那么在本篇文章当中,除了讲一些理论知识外,更多的是用实际的代码来验证这些理论。
2.本文提纲
· 1.开篇前言
· 2.本文提纲
· 3.Application
· 4.Window
· 5.Dispatcher及多线程
· 6.类继承结构
· 7.WPF的逻辑树和视觉树
· 8.本文总结
. 9.系列进度
3.Application
一.介绍
WPF和 传统的WinForm 类似, WPF 同样需要一个 Application 来统领一些全局的行为和操作,并且每个 Domain (应用程序域)中只能有一个 Application 实例存在。和 WinForm 不同的是 WPF Application 默认由两部分组成 : App.xaml 和 App.xaml.cs,这有点类似于 Delphi Form(我对此只是了解,并没有接触过Delphi ),将定义和行为代码相分离。当然,这个和WebForm 也比较类似。XAML 从严格意义上说并不是一个纯粹的 XML 格式文件,它更像是一种 DSL(Domain Specific Language,领域特定语言),它的所有定义都直接映射成某些代码,只是具体的翻译工作交给了编译器完成而已。WPF应用程序由System.Windows.Application类来进行管理。
二.创建WPF应用程序
创建WPF应用程序有两种方式:
1、Visual Studio和Expression Blend默认的方式,使用App.xaml文件定义启动应用程序
App.xaml文件的内容大致如下所示:
2、可以自已定义类,定义Main方法实现对WPF应用程序的启动
在项目中添加一个类,类的代码如下,在项目选项中,设定此类为启动项。
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Data;
using System.Linq;
using System.Windows;
namespace WPFApplications
{
///
/// Interaction logic for App.xaml
///
public partial class App : Application
{
[STAThread]
static void Main()
{
// 定义Application对象作为整个应用程序入口
Application app = new Application();
// 方法一:调用Run方法,参数为启动的窗体对象 ,也是最常用的方法
Window2 win = new Window2();
app.Run(win);
// 方法二:指定Application对象的MainWindow属性为启动窗体,然后调用无参数的Run方法
//Window2 win = new Window2();
//app.MainWindow = win;
//win.Show();
// win.Show()是必须的,否则无法显示窗体
//app.Run();
// 方法三:通过Url的方式启动
//app.StartupUri = new Uri("Window2.xaml", UriKind.Relative);
//app.Run();
}
}
}三、Application应用程序关闭
OnLastWindowClose(默认值): 最后一个窗体关闭或调用Application对象的Shutdown() 方法时,应用程序关闭。 OnMainWindowClose 启动窗体关闭或调用Application对象的Shutdown()方法时,应用程序关闭。(和C#的Windows应用程序的关闭模式比较类似) OnExplicitShutdown 只有在调用Application对象的Shutdown()方法时,应用程序才会关闭。 对关闭选项更改的时候,可以直接在App.xaml中更改:
<Application x:Class="WPFApplications.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
StartupUri="Window2.xaml"
ShutdownMode="OnExplicitShutdown">
<Application.Resources>
Application.Resources>
Application>同样你也可以在代码文件(App.xaml.cs)中进行更改,但必须注意这个设置写在app.Run()方法之前 ,如下代码:
app.ShutdownMode = ShutdownMode.OnExplicitShutdown;
app.Run(win);四、Application对象的事件
名称
描述
Activated
当应用程序成为前台应用程序时发生,即获取焦点。
Deactivated
当应用程序停止作为前台应用程序时发生,即失去焦点。
DispatcherUnhandledException
在异常由应用程序引发但未进行处理时发生。
Exit
正好在应用程序关闭之前发生,且无法取消。
FragmentNavigation
当应用程序中的导航器开始导航至某个内容片断时发生,如果所需片段位于当前内容中,则导航会立即发生;或者,如果所需片段位于不同 内容中,则导航会在加载了源 XAML 内容之后发生。
LoadCompleted
在已经加载、分析并开始呈现应用程序中的导航器导航到的内容时发生。
Navigated
在已经找到应用程序中的导航器要导航到的内容时发生,尽管此时该内容可能尚未完成加载。
Navigating
在应用程序中的导航器请求新导航时发生。
NavigationFailed
在应用程序中的导航器在导航到所请求内容时出现错误的情况下发生。
NavigationProgress
在由应用程序中的导航器管理的下载过程中定期发生,以提供导航进度信息。
NavigationStopped
在调用应用程序中的导航器的 StopLoading 方法时发生,或者当导航器在当前导航正在进行期间请求了一个新导航时发生(没大用到)。
SessionEnding
在用户通过注销或关闭操作系统而结束 Windows 会话时发生。
Startup
在调用 Application 对象的 Run 方法时发生。
应用程序的事件处理可以:
1、在App.xaml中做事件的绑定,在App.xaml.cs文件中添加事件的处理方法
在App.xaml文件中:
<Application x:Class="WPFApplications.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
StartupUri="Window1.xaml"
Startup="Application_Startup"
Exit="Application_Exit"
DispatcherUnhandledException="Application_DispatcherUnhandledException">
<Application.Resources>
Application.Resources>
Application>在App.xaml.cs文件中:
public partial class App : Application
{
[STAThread]
static void Main()
{
// 定义Application对象作为整个应用程序入口
Application app = new Application();
// 方法一:调用Run方法,参数为启动的窗体对象 ,也是最常用的方法
Window2 win = new Window2();
app.Run(win);
}
private void Application_DispatcherUnhandledException(object sender, System.Windows.Threading.DispatcherUnhandledExceptionEventArgs e)
{ }
private void Application_Exit(object sender, ExitEventArgs e)
{ }
}2、在自定义的类中可以做正常的C#的事件绑定:
public partial class App : Application
{
[STAThread]
static void Main()
{
// 定义Application对象作为整个应用程序入口
Application app = new Application();
// 调用Run方法,参数为启动的窗体对象 ,也是最常用的方法
Window2 win = new Window2();
app.Startup += new StartupEventHandler(app_Startup);
app.DispatcherUnhandledException += new System.Windows.Threading.DispatcherUnhandledExceptionEventHandler(app_DispatcherUnhandledException);
app.Run(win);
}
static void app_DispatcherUnhandledException(object sender, System.Windows.Threading.DispatcherUnhandledExceptionEventArgs e)
{
throw new NotImplementedException();
}
static void app_Startup(object sender, StartupEventArgs e)
{
throw new NotImplementedException();
}
}如果通过XAML启动窗体的话,也会编译成为为如下的程序,默认路径为Debug文件夹得App.g.cs文件:
public partial class App : System.Windows.Application {
///
/// InitializeComponent
///
[System.Diagnostics.DebuggerNonUserCodeAttribute()]
public void InitializeComponent() {
#line 4 "..\..\App.xaml"
this.StartupUri = new System.Uri("Window5.xaml", System.UriKind.Relative);
#line default
#line hidden
}
///
/// Application Entry Point.
///
[System.STAThreadAttribute()]
[System.Diagnostics.DebuggerNonUserCodeAttribute()]
public static void Main() {
WPFApplications.App app = new WPFApplications.App();
app.InitializeComponent();
app.Run();
}
}五、WPF应用程序生存周期
当然这幅图也只是简单的概括了WPF的执行顺序和生命周期,具体还要细致研究才是。
4.Window
一、窗体类基本概念
对于WPF应用程序,在Visual Studio和Expression Blend中,自定义的窗体均继承System.Windows.Window类.大家都可能听说过或者看过Applications = Code + Markup: A Guide to the Microsoft Windows Presentation Foundation这本书,它里面就是用XAML和后台代码两种形式来实现同一个功能,那么我们这里定义的窗体也由两部分组成:
1、 XAML文件,在这里面通常全部写UI的东西(希望大家还记得这两幅图)
2、后台代码文件
namespace WPFApplications
{
///
/// Interaction logic for Window5.xaml
///
public partial class Window5 : Window
{
public Window5()
{
InitializeComponent();
}
private void btnOK_Click(object sender, RoutedEventArgs e)
{
lblHello.Content = "Hello World Changed";
}
}
}也可以将后台代码放在XAML文件中,上面的例子可以改写为:
<Window x:Class="WPFApplications.Window5"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window5" Height="300" Width="300">
<StackPanel>
<Label x:Name="lblHello">Hello,World!Label>
<Button x:Name="btnOK" Width="88" Height="22" Content="Click"
Click="btnOK_Click"/>
<x:Code>
void btnOK_Click(object sender, System.Windows.RoutedEventArgs e)
{
lblHello.Content = "Hello World Changed";
}
]]>
x:Code>StackPanel>
Window>二、窗体的生命周期
1、显示窗体
- 构造函数
- Show()、ShowDialog()方法:Show()方法显示非模态窗口,ShowDialog()方法显示模态窗口,这个基本和WinForm类似
- Loaded事件:窗体第一次Show()或ShowDialog()时引发的事件,通常在此事件中加载窗体的初始化数据,但如果用了MVVM模式,基本就不在这里面写。
2、关闭窗体
- Close()方法:关闭窗体,并释放窗体的资源
- Closing事件、Closed事件:关闭时、关闭后引发的事件,通常在Closing事件中提示用户是否退出等信息。
3、窗体的激活
- Activate()方法:激活窗体
- Activated、Deactivated事件:当窗体激动、失去焦点时引发的事件
4、窗体的生命周期
为了证实上面的结论,我们用下面的代码进行测试:
public partial class Window3 : Window
{
public Window3()
{
this.Activated += new EventHandler(Window1_Activated);
this.Closing += new System.ComponentModel.CancelEventHandler(Window1_Closing);
this.ContentRendered += new EventHandler(Window1_ContentRendered);
this.Deactivated += new EventHandler(Window1_Deactivated);
this.Loaded += new RoutedEventHandler(Window1_Loaded);
this.Closed += new EventHandler(Window1_Closed);
this.Unloaded += new RoutedEventHandler(Window1_Unloaded);
this.SourceInitialized += new EventHandler(Window1_SourceInitialized);
InitializeComponent();
}
void Window1_Unloaded(object sender, RoutedEventArgs e)
{
Debug.WriteLine("Unloaded");
}
void Window1_SourceInitialized(object sender, EventArgs e)
{
Debug.WriteLine("SourceInitialized");
}
void Window1_Loaded(object sender, RoutedEventArgs e)
{
Debug.WriteLine("Loaded");
}
void Window1_Deactivated(object sender, EventArgs e)
{
Debug.WriteLine("Deactivated");
}
void Window1_ContentRendered(object sender, EventArgs e)
{
Debug.WriteLine("ContentRendered");
}
void Window1_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{
Debug.WriteLine("Closing");
MessageBoxResult dr = MessageBox.Show("Cancel the window?",
"Answer", MessageBoxButton.YesNo, MessageBoxImage.Question);
if (dr == MessageBoxResult.No)
{
e.Cancel = true;
}
}
void Window1_Closed(object sender, EventArgs e)
{
Debug.WriteLine("Closed");
}
void Window1_Activated(object sender, EventArgs e)
{
Debug.WriteLine("Activated");
}
}执行结果为:
WPF窗体的详细的属性、方法、事件请参考MSDN,有很多的属性、方法、事件与Windows应用程序中 System.Windows.Forms.Form类颇为相似,其中常用的一些属性、方法、事件有:
- 窗体边框模式(WindowStyle属性)和是否允许更改窗体大小(ResizeMode属性) 。
- 窗体启动位置(WindowStartupLocation属性)和启动状态(WindowState属性) 等。
- 窗体标题(Title属性)及图标 。
- 是否显示在任务栏(ShowInTaskbar)
- 始终在最前(TopMost属性)
5.Dispatcher及多线程
提到这个UI和后台线程交互这个问题,大家都可能在WinForm中遇到过,记得几年前我参加一个外资企业的面试,公司的其中一道题就是说在WinForm 中如何使用后台线程来操作UI,所以对这个问题比较记忆犹新。
WPF线程分配系统提供一个Dispatcher属性、VerifyAccess 和 CheckAccess 方法来操作线程。线程分配系统位于所有 WPF 类中基类,大部分WPF 元素都派生于此类,如下图的Dispatcher类:
WPF 应用程序启动后,会有两个线程:
- 一个是用来处理UI呈现(处理UI的请求,比如输入和展现等操作)。
- 一个用来管理 UI的 (对UI元素及整个UI进行管理)。
与 Dispatcher 调度对象想对应的就是 DispatcherObject,在 WPF 中绝大部分控件都继承自 DispatcherObject,甚至包括 Application。这些继承自 DispatcherObject 的对象具有线程关联特征,也就意味着只有创建这些对象实例,且包含了 Dispatcher 的线程(通常指默认 UI 线程)才能直接对其进行更新操作。
当我们尝试从一个非 UI 线程更新一个UI元素,会看到如下的异常错误。
XAML代码:
<Window x:Class="WPFApplications.Window2"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window2" Height="300" Width="300">
<StackPanel>
<Label x:Name="lblHello">Hello,World!Label>
StackPanel>
Window>后台代码:
public partial class Window2 : Window
{
public Window2()
{
InitializeComponent();
Thread thread = new Thread(ModifyUI);
thread.Start();
}
private void ModifyUI()
{
// 模拟一些工作正在进行
Thread.Sleep(TimeSpan.FromSeconds(5));
lblHello.Content = "Hello,Dispatcher";
}
}错误截图:
按照 DispatcherObject 的限制原则,我们改用 Window.Dispatcher.Invoke() 即可顺利完成这个更新操作。
private void ModifyUINew()
{
// 模拟一些工作正在进行
Thread.Sleep(TimeSpan.FromSeconds(5));
this.Dispatcher.BeginInvoke(DispatcherPriority.Normal,(ThreadStart)delegate()
{
lblHello.Content = "Hello,Dispatcher";
});
}如果在其他工程或者类中,我们可以用 Application.Current.Dispatcher.Invoke方法来完成同样的操作,它们都指向 UI Thread Dispatcher这个唯一的对象。
Dispatcher 同时还支持 BeginInvoke 异步调用,如下代码:
private void btnHello_Click(object sender, RoutedEventArgs e)
{
new Thread(() =>
{
Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Normal,
new Action(() =>
{
Thread.Sleep(TimeSpan.FromSeconds(5));
this.lblHello.Content = DateTime.Now.ToString();
}));
}).Start();
}
关于Dispatcher和WPF多线程,还有很多要讲,由于篇幅有限且精力有限,我这里只讲一些我们最常见的应用,同时包括Freezable 的处理等问题,大家可以查阅MSDN或者查阅国外相关的专题。
6.类继承结构
在WPF中常用的的控件类继承结构如下图所示(图中圆圈的表示抽象类,方框的表示实体类):
- System.Object 类:大家都知道在.Net中所有类型的根类型,在图中没有画出来,DispatcherObject 就继承于它,所以它是整个应用系统的基类。
- System.Windows.Threading.DispatcherObject 类:WPF 中的绝大多数对象是从 DispatcherObject 派生的,它提供了用于处理并发和线程的基本构造。WPF 是基于调度程序实现的消息系统。
- System.Windows.DependencyObject类:WPF基本所有的控件都实现了依赖属性,它表示一个参与依赖项属性系统的对象。
- System.Windows.Media.Visual类:为 WPF 中的呈现提供支持,其中包括命中测试、坐标转换和边界框计算等。
- System.Windows.UIElement 类:UIElement 是 WPF 核心级实现的基类,该类建立在 Windows Presentation Foundation (WPF) 元素和基本表示特征基础上。
- System.Windows.FrameworkElement类:为 Windows Presentation Foundation (WPF) 元素提供 WPF 框架级属性集、事件集和方法集。此类表示附带的 WPF 框架级实现,它是基于由UIElement定义的 WPF 核心级 API 构建的。
- System.Windows.Controls.Control 类:表示 用户界面 (UI) 元素的基类,这些元素使用 ControlTemplate 来定义其外观。
- System.Windows.Controls.ContentControl类:表示包含单项内容的控件。
- System.Windows.Controls.ItemsControl 类:表示一个可用于呈现项的集合的控件。
- System.Windows.Controls.Panel类:为所有 Panel 元素(布局)提供基类。使用 Panel 元素在 Windows Presentation Foundation (WPF) 应用程序中放置和排列子对象。
- System.Windows.Sharps.Sharp类:为 Ellipse、Polygon 和 Rectangle 之类的形状元素提供基类。
除了上面的图以外,还有几个命名空间也很重要,如下:
- System.Windows.Controls.Decorator 类:提供在单个子元素(如 Border 或 Viewbox)上或周围应用效果的元素的基类。
- System.Windows.Controls.Image 类:表示显示图像的控件。
- System.Windows.Controls.MediaElement类:表示包含音频和 /或视频的控件。
7.WPF的逻辑树和视觉树
关于这部分的内容讲起来就比较多了,正如上次大家的留言里说的一样,这个内容如果拉开来讲肯定就要开几个篇幅,所以我们今天主要以讲清楚概念为重点,先看下面的一个XAML代码的例子:
<Window x:Class="WPFApplications.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="300" Width="300">
<StackPanel>
<Label>Hello,World!Label>
StackPanel>
Window>上面这个UI非常的简单,Window是一个根结点,它有一个子结点StackPanel,StackPanel有一个子结点Label。注意Label下还有一个子结点string(LabelText),它同时也是一个叶子结点。这就构成了窗口的一个逻辑树。逻辑树始终存在于WPF的UI中,不管UI是用XAML编写还是用代码编写。WPF的每个方面(属性、事件、资源等等)都是依赖于逻辑树的。
视觉树基本上是逻辑树的一种扩展。逻辑树的每个结点都被分解为它们的核心视觉组件。逻辑树的结点对我们来说是不可见的。而视觉树不同,它暴露了视觉的实现细节。下面是Visual Tree结构就表示了上面四行XAML代码的视觉树结构(下面这幅图片来源于WPF揭秘):
当然并不是所有的逻辑树结点都可以扩展为视觉树结点。只有从 System.Windows.Media.Visual或者System.Windows.Media.Visual3D继承的元素才能被视觉树所包含。其他的元素不能包含是因为它们本身没有自己的提交(Rendering)行为。在Windows Vista SDK Tools当中的XamlPad提供查看Visual Tree的功能。需要注意的是XamlPad目前只能查看以Page为根元素,并且去掉了SizeToContent属性的XAML文档。如下图所示:
在visual studio的命令行中输入xamlpad就可以进入如下的界面:
通过上图我们可以看到Visual Tree确实比较复杂,其中还包含有很多的不可见元素,比如ContentPresenter等。Visual Tree虽然复杂,但是在一般情况下,我们不需要过多地关注它。我们在从根本上改变控件的风格、外观时,需要注意Visual Tree的使用,因为在这种情况下我们通常会改变控件的视觉逻辑。 比如我们在自己写一些控件的时候,再比如我们对某些外观进行特别订制的时候。
WPF 中还提供了遍历逻辑树和视觉树的辅助类:System.Windows.LogicalTreeHelper和 System.Windows.Media.VisualTreeHelper。注意遍历的位置,逻辑树可以在类的构造函数中遍历。但是,视觉树必须在经过至少一次的布局后才能形成。所以它不能在构造函数遍历。通常是在OnContentRendered进行,这个函数为在布局发生后被调用。
其实每个Tree结点元素本身也包含了遍历的方法。比如,Visual类包含了三个保护成员方法VisualParent、 VisualChildrenCount、GetVisualChild。通过它们可以访问Visual的父元素和子元素。而对于 FrameworkElement,它通常定义了一个公共的Parent属性表示其逻辑父元素。特定的FrameworkElement子类用不同的方式暴露了它的逻辑子元素。比如部分子元素是Children Collection,有是有时Content属性,Content属性强制元素只能有一个逻辑子元素。为了弄清楚这些概念,我们就通过如下代码作为演示:
public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
PrintLogicalTree(0, this);
}
protected override void OnContentRendered(EventArgs e)
{
base.OnContentRendered(e);
PrintVisualTree(0, this);
}
void PrintLogicalTree(int depth, object obj)
{
// 打印空格,方便查看
Debug.WriteLine(new string(' ', depth) + obj);
// 如果不是DependencyObject,如string等类型
if (!(obj is DependencyObject)) return;
// 递归打印逻辑树
foreach (object child in LogicalTreeHelper.GetChildren(
obj as DependencyObject))
{
PrintLogicalTree(depth + 1, child);
}
}
void PrintVisualTree(int depth, DependencyObject obj)
{
//打印空格,方便查看
Debug.WriteLine(new string(' ', depth) + obj);
// 递归打印视觉树
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(obj); i++)
{
PrintVisualTree(depth + 1, VisualTreeHelper.GetChild(obj, i));
}
}
}结果为:
8.本文总结
本篇主要对Application、window、多线程、类继承结构、逻辑树与可视树等的理论和实际Demo进行了探讨,通过这一篇文章,我们可以大概了解WPF在这些元素上的处理,同时也给我后面的内容奠定了基础,后面会逐渐牵涉到实际的一些案例和新的概念,所以如果有不熟悉且对这个专题感兴趣的朋友可以仔细看一下这篇文章,在文章后面也会把本文用到的代码附加上去,大家可以下载下来进行测试。
最后圣殿骑士 会尽心尽力写好这个系列,同时由于是自己对这些技术的使用总结和心得体会,错误之处在所难免,怀着技术交流的心态,在博客园和51CTO发表出来,所以希望大家能够多多指点,这样在使一部分人受益的同时也能纠正我的错误观点,以便和各位共同提高,后续文章敬请关注!
9.系列进度(红色标示已发布)
· 5. 使用面板做布局(几种布局控件的XAML及CS代码,综合布局等)
· 6. 依赖属性、附加属性(基本、继承、元数据)
· 7. 路由事件、附加事件
· 8. 命令
· 9. WPF控件分类介绍与使用技巧(ContentControl、HeaderedContentControl…… Decorator)
· 10. 尺寸缩放、定位与变换元素
· 11. 资源
· 12. 数据绑定(基本、值转换、验证、集合的筛选、排序、分组、主从、数据提供者)
· 13. 样式
· 14. 模板
· 15. 多语言、皮肤和主题
· 16. 2D图形
· 17. 3D图形
· 18. 动画(几种动画的应用)
· 19. 音频、视频、语音
· 20. 文档、打印、报表
· 21. 用户控件和自定义控件
· 22. Win32、Windows Form以及ActiveX之间的互用性
· 23. 构建并部署应用程序(ClickOnce部署、微软setup /InstallShield+自动更新组件)
· 24. WPF的模式讲解及实例(MVC Demo)
· 25. WPF的模式讲解及实例(MVP Demo)
· 26. WPF的模式讲解及实例(MVVM Demo)
· 27. 性能优化(WPF项目的瓶颈)
· 28.一个完整WPF项目(普通架构版)
· 39. 一个完整WPF项目(MVVM架构版)
· 30. WPF 4.0新功能