锐英源软件
第一信赖

精通

英语

开源

擅长

开发

培训

胸怀四海 

第一信赖

当前位置:锐英源 / 开源技术 / WPF / WPF MVVM开发要点、WPF MVVM开发技巧
服务方向
人工智能数据处理
人工智能培训
kaldi数据准备
小语种语音识别
语音识别标注
语音识别系统
语音识别转文字
kaldi开发技术服务
软件开发
运动控制卡上位机
机械加工软件
软件开发培训
Java 安卓移动开发
VC++
C#软件
汇编和破解
驱动开发
联系方式
固话:0371-63888850
手机:138-0381-0136
Q Q:396806883
微信:ryysoft

锐英源精品原创,禁止全文或局部转载,禁止任何形式的非法使用,侵权必究。点名“简易百科”和闲暇巴盗用锐英源原创内容。

WPF MVVM开发要点、WPF MVVM开发技巧



本文来源于TechNet,谢谢原作者。里面有我的翻译和理解,对于初学者和有一定经验的朋友都有提升能力作用,欢迎关注和收藏。


介绍


模型视图 ViewModel是 WPF 的核心要点,允许开发人员将其应用程序代码与任何 UI 依赖项完全分离。
这意味着可以轻松地重新设计应用程序,并且还可以使应用程序更易于测试。 View代表任何前端用户界面控件(Window、Page、UserControl), Model代表应用程序中使用的类,而ViewModel 是请求、塑造和公开数据(作为属性和命令)的中间人。
 

关于示例项目

这篇文章链接到一个 TechNet 示例项目,您可以下载和浏览该项目。 它涵盖了 MVVM 的许多基本概念,以及一些常见的陷阱和解决方案。
我们的想法是下载示例,运行它以查看每个示例,然后通读此示例以及代码。当你完成时,你应该知道你需要知道的大部分内容 :)
  下载: http://gallery.technet.microsoft.com/Easy-MVVM-Examples-48c94de3 
如果你正在寻找更多好的 WPF 示例,您可能会喜欢 我的其他画廊示例项目  。我要问的是你给他们打分(星),谢谢。

注:实际上例子下载不到,想认真学习本文要点的找锐英源交流了。

 

经典 INotifyPropertyChanged


AddPerson mvvm
第一个示例是经典的 MVVM 配置,在基类 ( ViewModelBase )中实现 INotifyPropertyChanged
 
class ViewModelBase : INotifyPropertyChanged
{
    internal void RaisePropertyChanged(string prop)
    {
        if (PropertyChanged != null) { PropertyChanged(this, new PropertyChangedEventArgs(prop)); }
    }
    public event PropertyChangedEventHandler PropertyChanged;

该基类包含所有其他派生 ViewModel 的所有属性使用的公共代码,如下所示。
 
string _TextProperty1;
public string TextProperty1
{
    get
    {
        return _TextProperty1;
    }
    set
    {
        if (_TextProperty1 != value)
        {
            _TextProperty1 = value;
            RaisePropertyChanged("TextProperty1");
        }
    }
}

如您所见,它看起来像一个标准的 CLI 对象,但在 setter 中具有额外的RaisePropertyChanged方法。

在示例 1 中,ViewModel 由 View 本身附加在 XAML 中。请注意,这是可能的,因为 ViewModel 在其构造函数中不需要任何参数。
 
<Window . . .
        xmlns:vm="clr-namespace:MvvmExample.ViewModel"
        DataContext="{DynamicResource ViewModelMain}">
 
    <Window.Resources>
        <vm:ViewModelMain x:Key="ViewModelMain"/>
    </Window.Resources>

有一个ListBoxDataGridComboBox ,  所有控件的ItemsSource绑定到同一个集合,以及同一个SelectedItem
DataGrid 中任何属性值的更改都会反映在所有控件中。
当您在任何这些控件中更改选定的人时,您将看到所有三个一起更改。
TextBox 和 TextBlock 共享相同的属性,因此 TextBox 中的更改会反映在 TextBlock 中。
单击按钮添加用户,它显示在所有三个控件中。
虽然很多人会认为这都是由于我们“丰富”了 INPC 类,但 实际上大部分是通过通过 UI 更改值来触发的, 后面会解释。
  只有 Add user 命令依赖于 PropertyChanged 事件,它们来自不断变化的 ObservableCollection。

在代码隐藏中从事件处理程序切换窗口


从一个窗口导航到另一个窗口的传统方法是使用代码隐藏事件处理程序,这出现在传统的触发函数里。
这意味着您的代码与用户界面紧密耦合。
 
private void Button_Click(object sender, RoutedEventArgs e)
{
    var win = new Window1 { DataContext = new ViewModelWindow1(tb1.Text) };
    win.Show();
    this.Close();
}


MVVM 中最常见的第一个问题是如何从 ViewModel 调用 Window.Close() 等控件的方法
 
这将在下一个示例中显示。

注:这个在我写的代码里用到了消息。

用代码制作的 DataContext

 

第二个示例简单地展示了如何将 ViewModel 从代码附加到 DataContext ,由上面显示的上一个窗口完成,因为这个窗口是创建的。
 
var win = new Window1 { DataContext = new ViewModelWindow1(tb1.Text) };

在此示例中,ViewModel 在其构造函数中采用了一个字符串参数。构造函数中完成的工作是在绑定发生之前,所以我们可以填充 私有变量
 
public ViewModelWindow1(string lastText)
{
    _TestText = lastText;

此 ViewModel 派生自 ViewModelMain,具有额外的公共属性和命令,用于从基类中提取值并更新此新属性。Button 命令在CommandParameter

中传入来自 UI 的值。这使我们不必直接从代码中引用控件。
 
<Button Content="Change Text" Command="{Binding ChangeTextCommand}" CommandParameter="{Binding SelectedItem, ElementName=dg1}"/>

在此示例中,所选人员用于为公共属性TestText 设置值。

此命令在页面完成绑定 后触发,因此我们以公共属性为目标,因此触发了 PropertyChanged 事件

这样与该属性相关的绑定就知道它们需要更新,否则更改将永远不会反映在 UI 中。
 
void ChangeText(object selectedItem)
{
    if (selectedItem == null)
        TestText = "Please select a person";
    else
    {
        var person = selectedItem as Person;
        TestText = person.FirstName + " " + person.LastName;
    }
}

注:界面带参数,参数和属性绑定,这带来了灵活处理机制。

从 ViewModel 关闭窗口


第二个示例展示了一种使用 Attached Behavior从 ViewModel关闭窗口 的好方法。 “关闭窗口”行为是从一个名为 DialogResult的附加属性中添加的。
 
public static class DialogCloser
{
    public static readonly DependencyProperty DialogResultProperty =
    DependencyProperty.RegisterAttached(
    "DialogResult",
    typeof(bool?),
    typeof(DialogCloser),
    new PropertyMetadata(DialogResultChanged));
 
    private static void DialogResultChanged(
    DependencyObject d,
    DependencyPropertyChangedEventArgs e)
    {
        var window = d as Window;
        if (window != null)  window.Close();
    }
    public static void SetDialogResult(Window target, bool? value)
    {
        target.SetValue(DialogResultProperty, value);
    }
}

然后将其附加到我们要控制的 Windows:

<Window x:Class="MvvmExample.ViewModel.Window1" WindowStartupLocation="CenterScreen"
        Title="Window1" Height="300" Width="400"
        xmlns:helpers="clr-namespace:MvvmExample.Helpers"
helpers:DialogCloser.DialogResult="{Binding CloseWindowFlag}">

由于我们所有的 Windows 都使用 ViewModel,它们都继承自ViewModelBase,我们可以将这个通用的 window close 属性和 Close 方法放在基类中

bool? _CloseWindowFlag;
public bool? CloseWindowFlag
{
    get { return _CloseWindowFlag; }
    set
    {
        _CloseWindowFlag = value;
        RaisePropertyChanged("CloseWindowFlag");
    }
}
 
public virtual void CloseWindow(bool? result = true)
{
    Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Background, new Action(() =>
    {
        CloseWindowFlag = CloseWindowFlag == null
            ? true
            : !CloseWindowFlag;
    }));
}

我们实际的ViewModel 现在可以处理导航和关闭窗口,如下所示:

void NextExample(object parameter)
{
    var win = new Window2();
    win.Show();
    CloseWindow();
}

注:上面的代码就有点绕了,如果没有面向对象思维理解不了,分派器是ViewMode到View的关键,如果看不明白,请找锐英源软件。

使用 DependencyObject 而不是 INPC

 

此示例显示了 INotifyPropertyChanged- DependencyObject和 Dependency Properties的替代方法。

class ViewModelWindow2 : DependencyObject
{
    public Person SelectedPerson
    {
        get { return (Person)GetValue(SelectedPersonProperty); }
        set { SetValue(SelectedPersonProperty, value); }
    }
 
    public static readonly DependencyProperty SelectedPersonProperty =
        DependencyProperty.Register("SelectedPerson", typeof(Person),
        typeof(ViewModelWindow2), new UIPropertyMetadata(null));

依赖属性被认为是一种“更丰富”的绑定方法,因为 Register 方法还具有 PropertyChangedCoerce委托方法的多个重载。

一般 MVVM 使用的 依赖属性的主要缺点是它们需要在 UI 层上处理

有关INPC 与 DP 辩论的更多信息,请阅读以下内容:

 

通过 CanExecute 控制应用程序

这个例子还展示了一个命令如何通过它的 CanExecute 委托来控制一个按钮是否被启用。
这再次远离控制器,更多地进入行为。
如果不满足条件,甚至无法触发该操作,并且当按钮被禁用时,用户很清楚这一点。

在此示例中,我们没有使用 command 参数,而是依靠 ViewModel 属性来填充所选项目。
如果没有,CanExecute方法返回false,这 会禁用按钮
全部封装在命令中,代码干净整洁。

public ViewModelWindow2()
{
    People = FakeDatabaseLayer.GetPeopleFromDatabase();
    NextExampleCommand = new RelayCommand(NextExample, NextExample_CanExecute);
}
 
bool NextExample_CanExecute(object parameter)
{
    return SelectedPerson != null;
}

 注:这个有意思,刚好开发里需要。

 

通过依赖属性关闭附加属性

在此示例中,要关闭窗口,我们仍然使用 Window XAML 中的Attached Property,但该属性是 ViewModel 中的 Dependency Property

public bool? CloseWindowFlag
{
    get { return (bool?)GetValue(CloseWindowFlagProperty); }
    set { SetValue(CloseWindowFlagProperty, value); }
}
 
// Using a DependencyProperty as the backing store for CloseWindowFlag.  This enables animation, styling, binding, etc...
public static readonly DependencyProperty CloseWindowFlagProperty =
    DependencyProperty.Register("CloseWindowFlag", typeof(bool?), typeof(ViewModelWindow2), new UIPropertyMetadata(null));

 
简单使用如下:

void NextExample(object parameter)
{
    var win = new Window3(SelectedPerson);
    win.Show();
    CloseWindowFlag = true;
}

将 POCO 对象与 MVVM 一起使用

 

WPF/MVVM 术语中的 POCO 类是不提供任何 PropertyChanged 事件的类。

class PocoPerson
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public int Age { get; set; }
}

这通常是遗留代码模块,或从 WinForms 转换而来。
下一个示例演示了当您不使用 PropertyChanged 事件时事情是如何开始中断的。

起初,一切似乎都很好。选中的项目全部更新,您可以更改现有人员的属性,并通过DataGrid添加新人员。

这些操作都是基于 UI 的操作,它们会更改TextBox.Text等 依赖属性,并自动触发PropertyChanged事件。但是TextBox实际上应该显示一个 时间戳,由Dispatcher Timer后面的代码设置。

public ViewModelWindow3(Person person)
{
    . . . snip . . .
 
    timer = new DispatcherTimer();
    timer.Interval = TimeSpan.FromSeconds(1);
    timer.Tick += new EventHandler(timer_Tick);
    timer.Start();
}
 
void timer_Tick(object sender, EventArgs e)
{
    TextProperty1 = DateTime.Now.ToString();
}

此外,单击按钮添加新人似乎不起作用,但确实有效。

如果您随后尝试在 DataGrid 中添加用户,则绑定会更新并显示先前添加的用户。 都有点琐碎。

 
自定义自关闭事件技巧

作为替代方案,此 ViewModel 有一个自定义事件,用于表示关闭,在附加 ViewModel 时在后面的代码中处理。

public Window3(Person person)
{
    InitializeComponent();
    var vm = new ViewModelWindow3(person);
    DataContext = vm;
    vm.CloseWindowEvent += new System.EventHandler(vm_CloseWindowEvent);
}
 
void vm_CloseWindowEvent(object sender, System.EventArgs e)
{
    this.Close();
}


修复一个属性以显示差异

 
以下窗口 与前面的 POCO 示例几乎相同。但是 TextProperty1 现在会 在其设置器中 触发 PropertyChanged 事件。

set
{
    if (_TextProperty1 != value)
    {
        _TextProperty1 = value;
        RaisePropertyChanged("TextProperty1"); //The fix
    }
}

 
现在您会发现时间戳属性显示在 TextBox 上,如下所示。 但是,随着来自 INPC 界面的事件,其他 UI 绑定现在更加破碎。 (尝试示例项目)

 

一种更干净的“界面”关闭窗口的方法

如果您认为这是它所属的地方,那么关闭窗口的行为可以很高兴地保留在窗口中。

始终确保任何 ViewModel 具有上述预期事件的一种方法是让我们的 ViewModel(理想情况下在 ViewModelBase 中)继承 定义事件的接口。

然后,我们可以安全地将任何 ViewModel 转换回此接口,并将处理程序附加到预期事件。

public Window4()
{
    InitializeComponent();
    DataContextChanged += new DependencyPropertyChangedEventHandler(Window4_DataContextChanged);
}
 
void Window4_DataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
{
    var dc = DataContext as IClosableViewModel;
    dc.CloseWindowEvent += new EventHandler(dc_CloseWindowEvent);
}

注:vm和v之间的互动比较麻烦,这里给出的方法大家要记好。   

如何使用已关闭的业务对象(数据库层、Web 服务)

从所有这些中学到的最重要的信息是,如果您可以将您使用的基(模型)类转换为 INPC 属性,请立即执行。您将为自己省去一个痛苦的世界,通过包装器进行不必要的数据编组,以及一大堆代码,这些代码可能会给您的实现带来错误和依赖关系。

但是,如果您有一个处理所有工作的业务对象,例如 现有的数据库模块Web 服务客户端,该怎么办?

因此,这可能是一个“封闭对象”,您无法在其属性上使用 INPC 进行丰富。

在这种情况下,您必须依靠wrappers 和 polling

最后一个示例 ViewModel 充当前端控件和“人事管理”业务对象之间的牧羊人。它仅公开以下方法:
  • 获取雇员 ()
  • AddPerson(人)
  • 删除人(人)
  • 更新人(人)

它还有一个公共 POCO 字符串属性ReportTitle

最后,有一个“状态”枚举(离线、在线),它会 定期从 [后台线程] Timer切换。

包装 POCO


首先,我们需要整理进出业务对象的数据。我们需要将数据包装在 编组属性中。

由于我们只有一个员工列表的GET 方法,我们只需要一个带有 get 方法的公共属性来填充我们的 DataGrid:

ObservableCollection<PocoPerson> _People;
public ObservableCollection<PocoPerson> People
{
    get
    {
        _People = new ObservableCollection<PocoPerson>(personnel.GetEmployees());
        return _People;
    }
}

公共字符串属性“ Report Title ”是一个双向传递包装器 (getter 和 setter)。它将值编组进出。

这将我们带回到我们的第一个 POCO 示例,这对于某些操作来说已经足够了,但是对“后端”数据的更改 不会自动反映在 UI 中

有一个Label也绑定到此属性,但同样它只有效 ,因为更改是 UI 启动的,如前面的示例中所述。

public string ReportTitle
{
    get
    {
        return personel.ReportTitle;
    }
    set
    {
        if (personel.ReportTitle != value)
        {
            personnel.ReportTitle = value;
            RaisePropertyChanged("ReportTitle");
        }
    }
}

轮询 LITTLE


最后一个属性(BoStatus)表示业务对象“ Status ”属性(personel.Status),但是这个值是由另一个线程不断更新的,所以我们通常会错过用户界面中的更新。

唯一的答案是轮询属性以进行更改。 

因此,BoStatus不是包装器,而是“最后已知”值的存储

MvvmExample.Model.PersonelBusinessObject.StatusType _BoStatus;
public MvvmExample.Model.PersonelBusinessObject.StatusType BoStatus
{
    get
    {
        return _BoStatus;
    }
    set
    {
        if (_BoStatus != value)
        {
            _BoStatus = value;
            RaisePropertyChanged("BoStatus");
        }
    }
}

我们使用Timer来经常检查属性的变化。这可能看起来很昂贵,但在计算机处理器方面确实不是。

当我们检测到更改时,我们更新公共属性 BoStatus,它将新值保存在本地,并 触发 PropertyChanged 事件

任何使用这个属性的东西都会更新它们的绑定,并调用 getter 来获取新值。

void CheckStatus(object sender, EventArgs e)
{
    if (_BoStatus != personel.Status)
        BoStatus = personel.Status;
}

使用我们业务对象的CRUD方法实际上取决于 您自己的个人设计和实现。

然而,作为最后一个例子,我包含了一个非常简单的方法来做到这一点, 几乎没有代码

几乎无代码的主/细节 CRUD 控制

 
此示例显示了一个完整且几乎无代码的主/详细信息、CRUD 控制(用于数据库、Web 服务等) 通过主/子信息,我们的意思是有一个对象的主列表。然后选择一个列表项以在单独的框中获取项目详细信息。 CRUD是指创建更新和 删除功能。您对集合或数据库中的对象执行的三件事。 通常,在大型应用程序中,我们会为编辑表单设计一个独立的用户控件,但这里有一个快速技巧来生成整个、完全连接的编辑表单,使用 ItemsTemplateItemsControl

实际的编辑网格是一个 DataTemplate,而不是一个实际的控件。此 DataTemplate用作 ItemsControlItemTemplate

<ItemsControl BindingGroup="{Binding UpdateBindingGroup, Mode=OneWay}" ItemTemplate="{StaticResource UserGrid}" ItemsSource="{Binding SelectedPerson, Converter={StaticResource SelectedItemToItemsSource}}"/>

ItemsSource与我们的ViewModel 的SelectedPerson属性相关联 。 如果您只是在编辑而不是添加新用户,则可以将其直接绑定到 DataGrid的SelectedItem属性,但稍后您会看到为什么我们不这样做。在ItemsControl上定义了 一个BindingGroup,我们稍后会使用它来 取消或提交表单中的更改。 ItemsSource 需要一个集合,而不是 SelectedItem (SelectedPerson)。所以为了使这个技巧奏效,我们使用一个简单的转换器将选定的人包装到一个集合中。

public class SelectedItemToItemsSource : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        if (value == null) return null;
        return new List<object>() { value };
    }
 
    public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}


此方法表示如果没有选中项,则没有编辑表单。
 

这消除了对可见性转换器或触发器显示和隐藏表单的需要!


如何绑定以及何时更新

 

绑定到所选人员的属性,只是 生成项目 的直接DataContext ——直接且简单

<TextBox Text="{Binding FirstName, BindingGroupName=Group1, UpdateSourceTrigger=Explicit}" Grid.Column="1"/>

但是,要返回ViewModel并更新我们的 “人员”业务对象,我们需要进一步向上遍历 VisualTree,为此我们使用RelativeSource

<Button Foreground="Red" Content="Cancel" Command="{Binding DataContext.CancelCommand, RelativeSource={RelativeSource AncestorType=ItemsControl}}" Margin="4,0"/>

这个绑定返回,寻找ItemsControl的第一个实例。然后我们通过它的 DataContext引用属性。

为了允许我们取消编辑,绑定的UpdateSourceTrigger更改为 Explicit

可以防止源数据的任何更新,直到我们手动调用 绑定上的更新。

由于这些是自动生成的控件和绑定,我们通常必须使用 VisualTreeHelper来寻找 VisualTree 中的控件,以便我们可以定位并调用绑定上的 Update 方法... Yuk... o_O 

相反,我们使用的是绑定组,此组是我们之前附加到 ItemsControl的。

DataTemplate中的每个绑定还包括对该 组名称的引用:

<TextBox Text="{Binding . . . BindingGroupName=Group1, . . ." />

在 MVVM 场景中使用它的技巧是这个BindingGroup需要在 ViewModel中定义,并通过 binding传递到 ItemsControl 。如果要由 DataTemplate 绑定

使用,它 必须已经在等待

<ItemsControl BindingGroup="{Binding UpdateBindingGroup, Mode=OneWay}" . . .  />

这只是我们 ViewModel 的另一个属性,我们可以在 ViewModel 中使用它并从 UI 更新,因为绑定由 ItemsControl 创建和销毁。

BindingGroup _UpdateBindingGroup;
public BindingGroup UpdateBindingGroup
{
    get
    {
        return _UpdateBindingGroup;
    }
    set
    {
        if (_UpdateBindingGroup != value)
        {
            _UpdateBindingGroup = value;
            RaisePropertyChanged("UpdateBindingGroup");
        }
    }
}
 
public ViewModelWindow5()
{
    . . .
 
    UpdateBindingGroup = new BindingGroup { Name = "Group1" };

在我们的命令处理方法中,我们可以更新或取消来自 ViewModel 的更改

void DoCancel(object param)
{
    UpdateBindingGroup.CancelEdit();

void DoSave(object param)
{
    UpdateBindingGroup.CommitEdit();

注:组、取消和提交普通人用的少,是一些技巧。

创建、更新、删除逻辑

要添加新用户,我们只需在 SelectedPerson 中创建一个新的“占位符”对象。
这显示并使用我们可以填写的新用户对象填充表单。
这就是我们将 ItemsControl ItemsSource 绑定到 SelectedPerson 而不是直接绑定到 DataGrid.SelectedItem 的原因。

如果新用户已删除/取消,我们只需将SeletedPerson设置回 null即可处理对象。

如果现有用户取消,我们在BindingGroup上 调用CancelEdit

如果保存了新用户,我们在 业务对象上调用AddUser,然后触发  People集合上的PropertyChanged ,用于获取来自Business Object的新列表。

如果现有用户已保存,我们在 Business Object上调用UpdateUser并在BindingGroup上调用ComitEdit 

如果现有用户已删除,我们 在 Business Object上调用DeleteUser ,然后在 People集合上触发PropertyChanged ,以获取来自Business Object的新列表。 例如,这里是我们为Save命令所做的:

void DoSave(object param)
{
    UpdateBindingGroup.CommitEdit();
    var person = SelectedPerson as PocoPerson;
    if (SelectedIndex == -1)
    {
        personel.AddPerson(person);
        RaisePropertyChanged("People"); // Update the list from the data source
    }
    else
        personel.UpdatePerson(person);
 
    SelectedPerson = null;
}

如果您要更新现有的 Person,CommitEdit 会更新现有集合中的属性。
它不会更新列表。CommitEdit 将更新现有数据和用户界面,因此我们不需要请求更新整个列表。

我希望这篇文章有助于解释 PropertyChanged 事件在 MVVM 中的作用,以及如何连接控件和导航。

注:MVVM的View也对数据库有整合能力,当然需要M配合,所以ItemsSource表示一个实体的能力,这个表示用在代码里,就让Model的数据更容易灵活使用。不好理解的话,先理解Combox的ItemSource。

友情链接
版权所有 Copyright(c)2004-2021 锐英源软件
公司注册号:410105000449586 豫ICP备08007559号 最佳分辨率 1024*768
地址:郑州大学北校区院(文化路97号院)内