zoukankan      html  css  js  c++  java
  • WPF MVVM 循序渐进

    文章申明:本文转载自https://my.oschina.net/unpluggedcoder/blog/536301,感谢原作者的知识分享,如有侵权麻烦联系本人删除,谢谢!    

      如果你问任何一个.NET开发者, 什么是最小的基础架构, 首先浮现的就是"三层架构"。 在这个框架中, 我们把项目分为三个逻辑层次: UI层, 业务逻辑层和数据访问层, 每一层都负责各自对应的功能。

    三层架构

    UI负责显示功能, 业务逻辑层负责校验, 数据访问层负责SQL语句。 3层架构有如下的好处:

    • 包容变化: 每一层的变化不会重复跨越到其它层次。
    • 重用性: 增强可重用性, 因为每一层都是分离, 自包容的独立实体

      MVVM是三层架构的一个演化。 我知道我没有一个历史去证明这点, 但是我个人对MVVM进行了演化和观察。 那我们先从三层基础架构开始, 去理解三层架构存在的问题, 看MVVM架构是如何解决这些问题, 然后升级到去创建一个自定义的MVVM框架代码。 下面是本文接下来的路线图。

    Road map of MVVM

      

      简单的三层架构示例和GLUE(胶水)代码问题:

      首先, 让我们来理解三层架构以及它存在的问题, 然后看MVVM如何解决这个问题。

      直觉和现实是两种不同的事物。 当你看到三层架构的图, 你首先的直觉是每个功能可能都分布在各自层次。 但是当你实际编写代码时, 有些层次被强迫去做一些它们不应该做的额外的工作(破坏了SOLID原则)。 如果你对SOLID原则还不熟悉可以参考这个视频: SOLID principle video(译者注: SOLID指Single responsibility, Open-closed, Liskov substitution, Interface segregation and Dependency inversion, 即单一功能、开闭原则、里氏替换、接口隔离以及依赖反转)。

    GLUE Code

    这部分额外工作就在UI与Model之间, 以及Model与Data access之间。 我们把这类代码称为"GLUE"(胶水, 译者注:由于作者全用大写字母表示, 因此后续延用GLUE)代码。 "GLUE"代码主要有两种逻辑类型: 鄙人浅见薄识, 如果你有更多的"GLUE"类型实例, 请在留言中指出。

    • 映射逻辑(绑定逻辑): 每一层通过属性、方法和集合和其它层链接。例如, 一个在UI层中名为“txtCustomerName”的Textbox控件,将其映射到customer类的"CustomerName"属性。
    txtCustomerName.text = custobj.CustomerName; // 映射代码

    现在谁应该拥有上述绑定逻辑代码,UI还是Model?开发者往往把这个代码推到UI层次中。

    • 转换逻辑:每个层次使用的数据格式都是不同的。比如一个Model类"Person"有一个性别属性,可取值分别为"F"(Female)和"M"(Male)分别代表女性和男性。但是在UI层中,希望将这个值可视化为一个复选框控件,勾选则代表男性,不勾选则代表女性。下面是一个转换代码示例。
    1 if (obj.Gender == "M")// 转换代码
    2 {
    3     chkMale.IsChecked = true;
    4 }
    5 else
    6 {
    7     chkMale.IsChecked = false;
    8 }

      大多数开发者最终会将"GLUE"代码写到UI层中。通常可以在后台代码中定位到这类代码,例如.cs文件。如果UI是XAML,则对应的XAML.cs包含GLUE代码;如果UI是ASPX,则对应的ASPX.cs包含GLUE代码,以此类推。

      那么问题来了:是UI负责这类GLUE代码吗?让我们看下WPF应用中的一个简单的三层结构例子,以及更详细的GLUE代码细节。

      下面是一个简单的模型类"Customer",它有三个属性“CustomerName”, “Amount” 和“Married” 。

    Customer class

      但是,当这个模型显示到UI上时它又表现如下。所以,你可以看出来它包含了该模型的所有属性,以及一些额外的元素:颜色标签和Married复选框控件。

    Customer UI

      下面有一张简单的表,左边是Model,右边是UI,中间是谈过的映射和转换逻辑。你可以看到前两行没有转换逻辑,只有映射逻辑,另外两行则同时包含转换逻辑和映射逻辑。

    ModelGLUE CODEUI
    Customer Name No conversion needed only Mapping Customer Name
    Amount No conversion needed only Mapping Amount
    Amount Mapping + Conversion logic. > 1500 = BLUE, < 1500 = RED
    Married Mapping + Conversion logic. True – Married, False - UnMarried

    这些转换和映射逻辑代码通常会在“xaml.cs”文件中。下面是上图对应的后台代码,你可以看到映射代码和颜色判定、性别格式转换代码。我在代码中用注释标注出来,这样你可以看到哪些是映射代码,哪些是转换代码。

     1 lblName.Content = o.CustomerName; // 映射代码
     2 lblAmount.Content = o.Amount; // 映射代码
     3 
     4 if (o.Amount > 2000) // 转换代码
     5 {
     6     lblBuyingHabits.Background = new SolidColorBrush(Colors.Blue);
     7 }
     8 else if (o.Amount > 1500) // 转换代码
     9 {
    10     lblBuyingHabits.Background = new SolidColorBrush(Colors.Red);
    11 }
    12 if (obj.Married == "Married") // 转换代码
    13 {
    14     chkMarried.IsChecked = true;
    15 }
    16 else
    17 {
    18     chkMarried.IsChecked = false;
    19 }

    现在这些GLUE代码存在的问题:

    • 单一责任原则被破坏(SRPViolation): 是UI负责这些GLUE代码吗?这种情况下改变了Amount数量,同时也需要修改UI代码。现在,数据的改变为什么会让我去修改UI的代码?这里可以闻到坏代码的味道。UI应该只在我修改样式,颜色和布局的时候才改变。
    • 重用性: 如果我想把同样的颜色逻辑和性别格式转换用到下面的编辑界面,我该怎么做?拷贝粘帖重复的代码?

    CustomerEdit

    如果我想走得更远一点,把这个GLUE代码用在不同的UI技术体系上,比如MVC、Windows Form或者Mobile应用上。

    Reusability

    但是这里跨UI技术平台的重用实际上是不可能的,因为每个平台UI背后都和各自的UI技术体系耦合得很紧密。

    比如,下面的后台代码是继承自“Windows”类,而“Windows”类是集成在WPF UI体系中。如果我们想在Web应用或者MVC中应用这些逻辑,却又无法去创建一个这样的类对象来使用。

    1 public partial class MainWindow : Window
    2 {
    3 // Behind code is here
    4 }

    那么我们要怎么重用后台代码?怎么遵循SRP原则?

    ###第一步:最简单的MVVM示例 - 把后台代码移到类中

    我想大部分开发者已经知道怎么解决这个问题。毫无疑问地把后台代码(GLUE代码)移到一个类库中。这个类库代表了描述了UI的属性和行为。任何移入到这个类库的代码都可以编译成DLL,然后被所有.NET项目(Windows, Web等等)所引用。 因此,在这一节我们将创建一个最简单的MVVM示例,然后在后续的章节中我们将基于这个示例创建更高级的MVVM示例。

    Simplest MVVM

    我们创建一个“CustomerViewModel”类来包含GLUE代码。“CustomerViewModel”类代表了你的UI,所以我们想保持它的属性和UI命名约定一致。你可以从下图看出来“CustomerViewModel”类的属性是如何从之前的CustomerModel类中映射过来: “TxtCustomerName”对应“CustomerName”,“TxtAmount”对应“Amount”等等。

    ViewModel

    下面是实际代码:

     1 public class CustomerViewModel 
     2 {
     3         private Customer obj = new Customer();
     4 
     5         public string TxtCustomerName
     6         {
     7             get { return obj.CustomerName; }
     8             set { obj.CustomerName = value; }
     9         }        
    10 
    11         public string TxtAmount
    12         {
    13             get { return Convert.ToString(obj.Amount) ; }
    14             set { obj.Amount = Convert.ToDouble(value); }
    15         }
    16 
    17 
    18         public string LblAmountColor
    19         {
    20             get 
    21             {
    22                 if (obj.Amount > 2000)
    23                 {
    24                     return "Blue";
    25                 }
    26                 else if (obj.Amount > 1500)
    27                 {
    28                     return "Red";
    29                 }
    30                 return "Yellow";
    31             }
    32          }
    33 
    34         public bool IsMarried
    35         {
    36             get
    37             {
    38                 if (obj.Married == "Married")
    39                 {
    40                     return true;
    41                 }
    42                 else
    43                 {
    44                     return false;
    45                 }
    46             }
    47         }
    48 }

    关于“CustomerViewModel”这个类有以下几点注意:

    • 类属性都以UI的命名方式来约定,这样看上去会更形象一些;
    • 这个类负责了类型转换的代码,使得UI看上去更轻量级。例如代码中的“TxtAmount”属性。在Model类中的“Amount”属性是数字,而转换的过程是在ViewModel类中完成。换句话说这个类负责了UI显示的所有职责(译者注:逻辑上的业务职责)让UI后台代码看上去更简洁;
    • 所有转换逻辑的代码都在这个类中,例如“LblAmountColor”属性和“IsMarried”属性;
    • 所有的属性数据都保持了简单的字符类型,这样可以在大多UI技术平台上适用。例如,“LblAmountColor”属性把颜色值用字符串来传递,这样可以在任何UI类型中重用,同时我们也保持了最小的数据共性。

    现在“CustomerViewModel”类包含了所有的后台代码逻辑,我们可以创建这个类的对象并绑定到UI元素上。你可以在下面代码看到我们只剩下了映射逻辑的代码部分,而转换逻辑的"GLUE"代码已经没有了。

    1 private void DisplayUi(CustomerViewModel o)
    2 {
    3     lblName.Content = o.TxtCustomerName;
    4     lblAmount.Content = o.TxtAmount;
    5     BrushConverter brushconv = new BrushConverter();
    6     lblBuyingHabits.Background = brushconv.ConvertFromString(o.LblAmountColor) as SolidColorBrush;
    7     chkMarried.IsChecked = o.IsMarried;
    8 }

    ###第二步:添加绑定 - 消灭后台代码

    第一步的方法很好,但是我们知道后台代码仍然还有问题,在WPF中消灭所有后台代码是完全可能的。接下来WPF绑定和命令登场了。

    WPF以其绑定(Binding)、命令(Commands)和声明式编程(Declarative programming)而著称。声明式编程意味着你可以使用XMAL来表达你的C#代码,而不用编写完整的C#代码。绑定功能帮助一个WPF对象连接到其它的WPF对象,从而他们可以发送和接收数据。

    当前的映射C#代码有三个步骤:

    • 导入: 我们要做的第一件事情是导入“CustomerViewModel”名称空间。
    • 创建对象: 下一步要创建“CustomerViewModel”类的对象。
    • 绑定代码: 最后将WPF UI绑定到这个ViewModel对象。

    下面表格展示了C#代码和与其对应相同的WPF XAML代码。

    步骤C#代码XAML代码
    导入 using CustomerViewModel; xmlns:custns="clr-namespace:CustomerViewModel;assembly=CustomerViewModel"
    创建对象 CustomerViewModelobj = new CustomerViewModel(); obj.CustomerName = "Shiv"; obj.Amount = 2000; obj.Married = "Married"; < Window.Resources> < custns: CustomerViewModel x:Key="custviewobj" TxtCustomerName="Shiv" TxtAmount="1000" IsMarried=”true”/>
    绑定对象 lblName.Content = o.CustomerName; < Label x:Name="lblName" Content="{Binding TxtCustomerName, Source={StaticResourcecustviewobj}}"/>

    你不需要写后台的代码,我们可以选中UI元素,按F4,如下图中选择指定绑定。这个步骤会把绑定代码插入到XAML中。

    Create Binding1

    选择“StaticResource”来指定映射,然后在UI元素和ViewModel对象之间指定绑定路径。

    Create Binding2

    这是你查看XAML.CS文件,它已经没有任何GLUE代码,同样也没有转换和映射代码。唯一的代码就是标准的WPF UI初始化代码。

    1 public partial class MVVMWithBindings : Window
    2 {
    3         public MVVMWithBindings()
    4         {
    5             InitializeComponent();
    6         }
    7 }        

    ###第三步:添加执行动作和“INotifyPropertyChanged”接口

    应用程序不仅仅只是有textboxs 和 labels, 同样还需要执行动作,比如按钮,鼠标事件等。 因此让我们添加一个按钮来看看如何把MVVM类应用起来。 我们在同样的UI上添加了一个‘Calculate tax’按钮,当用户按下按钮,它将根据“Sales Amount”值计算出税值并显示在界面上。

    Add Action

    因此为了在Model类实现上面的功能,我们添加一个“CalculateTax()”方法。当这个方法被执行,它根据薪水范围计算出税值,并将值保存在“Tax”属性值中。

     1 public class Customer
     2 { 
     3 ....
     4 ....
     5 ....
     6 ....
     7 private double _Tax;
     8 public double Tax
     9 {
    10 get { return _Tax; }
    11 }
    12         public void CalculateTax()
    13         {
    14     if (_Amount > 2000)
    15             {
    16                 _Tax = 20;
    17             }
    18             else if (_Amount > 1000)
    19             {
    20                 _Tax = 10;
    21             }
    22             else
    23             {
    24                 _Tax = 5;
    25             }
    26         }
    27 }

    由于ViewModel类是Model类的一个封装,因此我们需要在ViewModel类中创建一个方法来调用Model的“CalculateTax”方法。

     1 public class CustomerViewModel 
     2 {
     3         private Customer obj = new Customer();
     4 ....
     5 ....
     6 ....
     7 ....
     8         public void Calculate()
     9         {
    10             obj.CalculateTax();
    11         }
    12 }

    现在,我们想要在XAML的视图中调用这个“Calculate”方法,而不是在后台编写。不过你不能直接通过XAML调用“Calculate”方法,你需要用WPF的command类。

    我们通过使用绑定属性将数据发送给ViewModel类,而发送执行动作给ViewModel类则需要使用命令。

    Action and Properties

    所有从视图元素产生的动作都发送给command类,所以第一步是创建一个command类。为了创建自定义的command类,我们需要实现"ICommand"接口(如下图):

    "ICommand"接口有两个必须要重载的方法:“CanExecute” 和 “Execute”。在“Execute”中我们放的是希望动作发生时实际执行的逻辑代码(比如按钮按下,右键按下等)。在“CanExecute”中我们放的是验证逻辑来决定“Execute”代码是否应该执行。

    ICommand

     1 public class ButtonCommand : ICommand
     2 {
     3         public bool CanExecute(object parameter)
     4         {
     5       // When to execute
     6       // Validation logic goes here
     7         }
     8 
     9         public event EventHandler CanExecuteChanged;
    10 
    11         public void Execute(object parameter)
    12         {
    13 // What to Execute
    14       // Execution logic goes here
    15     }
    16 }

    现在所有的动作调用都发送到command类,然后被路由到ViewModel类。换句话说,command类需要组合ViewModel类(译注:command类需要一个ViewModel类的引用)。

    Route

    下面是简短的代码片段,有四点需要注意:

    1. ViewModel对象是作为一个私有的成员对象。
    2. 该ViewModel对象将通过构造函数参数的方式传递进来。
    3. 目前为止,我们没有在“CanExecute”中添加验证逻辑,它始终返回true。
    4. 在“Execute”方法中我们调用了ViewModel类的“Calculate”方法。
    public class ButtonCommand : ICommand
        {
            private CustomerViewModel obj; // Point 1
            public ButtonCommand(CustomerViewModel _obj) // Point 2
            {
                obj = _obj;
            }
            public bool CanExecute(object parameter)
            {
                return true; // Point 3
            }
            public void Execute(object parameter)
            {
                obj.Calculate(); // Point 4
            }
        }

    上面的command代码中,ViewModel对象是通过构造函数传递进来。所以ViewModel类需要创建一个command对象来暴露这个对象的“ICommand”接口。这个“ICommand”接口将被WPF XAML使用并调用。下面是一些关于“CustomerViewModel”类使用command类的要点:

    1. command类是“CustomerViewModel”类的私有成员。
    2. 在“CustomerViewModel”类的构造函数中将当前对象的实例传递给command类。在之前解释command类的一节中我们说了command类构造函数获取ViewModel类的实例。因此在这一节中我们正是将当前实例传递给command类。
    3. command对象是通过以“ICommand”接口的形式暴露出来,这样才可以被XAML所使用。
     1 using System.ComponentModel;
     2 
     3 public class CustomerViewModel 
     4 {
     5  6  7 private ButtonCommand objCommand; //  Point 1
     8         public CustomerViewModel()
     9         {
    10             objCommand = new ButtonCommand(this); // Point 2
    11         }
    12         public ICommand btnClick // Point 3
    13         {
    14             get
    15             {
    16                 return objCommand;
    17             }
    18         }
    19 20 21 }

    在你的UI中添加一个按钮,这样就可以把按钮的执行动作连接到暴露的“ICommand”接口。现在打开button的属性栏,选择command属性,右击创建一个数据绑定。

    Button Property

    然后选择静态资源(Static Resource),并将“ButtonCommand”附加到button上。

    Command Binding

    当你点击了Calculate Tax按钮,它就执行了“CalculateTax”方法。并将税值结果存在“_tax”变量中。关于“CalculateTax”方法代码,可以阅读前面的小节“第三步:添加执行动作和“INotifyPropertyChanged”接口”。

    换句话说,税值计算过程并不会自动通知给UI。所以我们需要从对象发送某种通知给UI,告诉它税值已经变化了,UI需要重新载入绑定值。

    Notification

    因此,在ViewModel类中我们需要发送INotify事件给视图。

    Notification

    为了让你的ViewModel类能够实现通知,我们必须做三件事情。这三件事情都在下面的代码注释中指出,例如Point1, Point2 和 Point3。

    Point1: 如下面代码那样实现“INotifyPropertyChanged”接口。一旦你实现了该接口,它就创建了对象的“PropertyChangedEventHandler”事件。

    Point2和3: 在“Calculate”方法中用“PropertyChanged”对象去触发事件,并在其中指定了某个属性的通知。在这里是“Tax”属性。安全起见,我们同样也要检查“PropertyChanged”是否不为空。

    public class CustomerViewModel : INotifyPropertyChanged // Point 1
    {
    ….
    ….
            public void Calculate()
            {
                obj.CalculateTax();
                if (PropertyChanged != null) // Point 2
                {
                    PropertyChanged(this,new PropertyChangedEventArgs("Tax"));
                // Point 3
                }
            }
    
            public event PropertyChangedEventHandler PropertyChanged;
    }

    如果你运行程序,你应该可以看见当点击按钮后“Tax”值被更新了。

    ###第四步:在ViewModel中解耦执行动作

    到目前为止,我们用MVVM框架创建了一个简单的界面。这个界面同时包含了属性和命令实现。我们拥有了一个视图,它的UI输入元素(例如textbox)通过绑定和ViewModel连接起来,它的任何执行动作(例如按钮点击)通过命令和ViewModel连接起来。ViewModel和内部的Model通讯。

    Simple MVVM

    但是在上面的结构中还有一个问题:command类和ViewModel类存在着过度耦合的情况。如果你还记得command类代码(我在下面贴出来了)中的构造函数是传递了ViewModel对象,这意味着这个command类无法被其它的ViewModel类所复用。

    public class ButtonCommand : ICommand
        {
            private CustomerViewModel obj; // Point 1
            public ButtonCommand(CustomerViewModel _obj) // Point 2
            {
                obj = _obj;
            }
    ......
    ......
    ......
    
    }

    More Actions

    但是在考虑了所有情况之后,让我们逻辑地思考下“什么是一个动作?”。它是一个事件,可以由用户从鼠标点击(左键或右键),按钮点击,菜单点击,功能键按下等。所以应该有一种方式通用化这些动作,并且让各种ViewModel有一种更通用的方法去绑定它。

    逻辑上讲,如果你认为任务动作是一些方法和函数的封装逻辑。那有什么是“方法”和“函数”的通用表达方式呢?......努力想想.......再想想.......“委托”,“委托”,没错,还是“委托”。

    我们需要两个委托,一个给“CanExecute”,另一个给“Execute”。“CanExecute”返回一个布尔值用来验证以及根据验证来使能(Enable)或者禁用(Disable)用户界面。“Execute”委托则将在“CanExecute”委托返回true时执行。

    public class ButtonCommand : ICommand
    {
            public bool CanExecute(object parameter) // Validations
            {
            }
            public void Execute(object parameter) // Executions
            {
            }
    }

    因此,换句话说,我们需要两个委托,一个返回布尔值,另一个执行动作并返回空。所以,创建一个“Func”和一个“Action”如何?“Func”和“Action”都可以用来创建委托。

    如果你还不熟悉Func和Action,可以看下下面这个视频。 (译注:作者在这里提供了一个YouTube的视频链接,大概说的就是C#中Func<>和Action<>这两个委托的区别,前者Func<>模版参数包含返回值类型,而Action<>表示无返回值的泛型委托,参见这里

    通过使用委托的方法,我们试着创建一个通用的command类。我们对command类做了三个修改(代码参见下面),同时我也标注了三点Point 1,2和3。

    Point1: 我们在构造函数中移除了ViewModel对象,改为接受两个委托,一个是“Func”,另一个是“Action”。“Func”委托用作验证(例如验证何时动作将被执行),而“Action”委托用来执行动作。两个委托都是通过构造函数参数传递进来,并赋值给类内部的对应私有成员变量。

    Point2和3: Func<>委托(WhentoExecute)被“CanExecute”调用,执行动作的委托Whattoexecute则是在“Execute”中被调用。

    public class ButtonCommand : ICommand
    {
    private Action WhattoExecute;
    private Func<bool> WhentoExecute;
            public ButtonCommand(Action What , Func<bool> When) // Point 1
            {
                WhattoExecute = What;
                WhentoExecute = When;
            }
    public bool CanExecute(object parameter)
            {
                return WhentoExecute(); // Point 2
            }
    public void Execute(object parameter)
            {
                WhattoExecute(); // Point 3
            }
    }
    public class ButtonCommand : ICommand
    {
    private Action WhattoExecute;
    private Func<bool> WhentoExecute;
            public ButtonCommand(Action What , Func<bool> When) // Point 1
            {
                WhattoExecute = What;
                WhentoExecute = When;
            }
    public bool CanExecute(object parameter)
            {
                return WhentoExecute(); // Point 2
            }
    public void Execute(object parameter)
            {
                WhattoExecute(); // Point 3
            }
    }

    在Model类中我们已经知道要执行什么了(例如“CalculateTax”),我们也创建一个简单的函数“IsValid”来验证“Customer”类是否有效。

    public class Customer
        {
    public void CalculateTax()
            {
    if (_Amount > 2000)
                {
                    _Tax = 20;
                }
    else if (_Amount > 1000)
                {
                    _Tax = 10;
                }
    else
                {
                    _Tax = 5;
                }
            }
    
    public bool IsValid()
            {
    if (_Amount == 0)
                {
    return false;
                }
    else
                {
    return true;
                }
            }
        }

    在ViewModel类中我们同时传递函数和方法给command类的构造函数,一个给“Func”,一个给“Action”。

    public class CustomerViewModel : INotifyPropertyChanged
    {
    private Customer obj = new Customer();
    privateButtonCommandobjCommand;
    publicCustomerViewModel()
            {
    objCommand = new ButtonCommand(obj.CalculateTax,
    obj.IsValid);
            }
    }

    这样使得框架更好,更解耦, 使得这个command类可以以一个通用的方式被其它ViewModel引用。下面是改善后的架构, 需要注意ViewModel如何通过委托(Func和Action)和command类交互。

    Final architecture

    ###第五步:利用PRISM

    最后如果有一个框架能帮助实现我们的MVVM代码那就更好了。PRISM就是其中一个可复用的框架。PRISM的主要用途是为了提供模块化开发,但是它提供了一个很好的“DelegateCommand”类拿来代替我们自己创建的command类。

    所以,第一件事情就是从这里下载PRISM,编译这个解决方案,添加“Microsoft.Practices.Prism.Mvvm.dll”和“Microsoft.Practices.Prism.SharedInterfaces.dll”这两个DLL库的引用。

    你可以去掉自定义的command类,导入“Microsoft.Practices.Prism.Commands”名称空间, 然后以下面代码的方式使用DelegateCommand。

    public class CustomerViewModel : INotifyPropertyChanged
    {
    private Customer obj = new Customer();
    private DelegateCommand  objCommand;
    public CustomerViewModel()
            {
    objCommand = new DelegateCommand(obj.CalculateTax,
                                            obj.IsValid);
            }
    …………
    …………
    …………
    …………
    
    }    
    }

    ###WPF MVVM的视频演示

    我同时也在下面的视频中从头演示了如何实现WPF MVVM(译注:一个YouTube链接...)。

    IMAGE ALT TEXT

    ###延伸阅读

    1. WPF/MVVM Quick Start Tutorial
    2. Simplifying the WPF TreeView by Using the ViewModel Pattern
    3. MVVM 应用程序中的多线程与调度
    4. 针对异步 MVVM 应用程序的模式:数据绑定
    5. 针对异步 MVVM 应用程序的模式:命令
    6. Using behaviours to bind to read-only properties in MVVM
    7. Cascading ComboBoxes in WPF using MVVM
    8. WPF/MVVM: Binding the IsChecked Property of a CheckBox to Several Other CheckBoxes
  • 相关阅读:
    LSMW TIPS
    Schedule agreement and Delfor
    Running VL10 in the background 13 Oct
    analyse idoc by creation date
    New Journey Prepare
    EDI error
    CBSN NEWS
    Listen and Write 18th Feb 2019
    Microsoft iSCSI Software Target 快照管理
    通过 Microsoft iSCSI Software Target 提供存储服务
  • 原文地址:https://www.cnblogs.com/djh5520/p/14036150.html
Copyright © 2011-2022 走看看