65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 C# 的 Windows Form 应用程序的 MVVM(模型-视图-视图模型)模式

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.84/5 (61投票s)

2012 年 4 月 13 日

CPOL

4分钟阅读

viewsIcon

306315

downloadIcon

18792

它是一种类似于 MVVM 的模式,但适用于 WinForms。

引言

MVVM 模式因其优点而受到 WPF/Silverlight 开发者的青睐。微软表示,MVVM

  • 将应用程序的业务逻辑和表示逻辑与其UI分开,使其更容易测试、维护和演进。
  • 允许开发人员和UI设计人员在开发各自的应用程序部分时进行更轻松的协作。(请查看链接

MVVM 是为 WPF 设计的,但有没有办法让 WinForms 也拥有这些优点呢?答案是肯定的(好消息,也许也不算太新)。Windows Form 应用程序也可以拥有这些优点,当然,这并不意味着完全是Model-View-ViewModelMVVM)模式。我搜索并找到许多方法(Google 搜索),其中一些很棒,有些则不太理想(!)。但我试图提供一个简单实用的解决方案。希望你会感兴趣。

背景

正如你所知,程序逻辑内容ASP.Net中是分开的。在WPF中,程序逻辑也与UI分开。在 Windows Forms 中,在我接触到MVVM模式之前,我倾向于使用分部类将程序逻辑UI分开。我对在WPF中使用该模式很感兴趣,因此我决定为WinForm也找到类似的方法,但我一直太忙,没有时间去研究它。有一天,我尝试将MVVM应用到WinForm中,这是我在业余时间研究该主题 3 小时的结果。

一个非常快速简短的 MVVM 回顾

MVVM 意为 Model-View-ViewModel(模型-视图-视图模型)

MVVM

1. View(视图): 指的是UI显示的所有元素。有些人说 View 是UI,另一些人说 View 是UI“但包含”逻辑(我喜欢后一种说法)。此外,它向 ViewModel 传递命令。

2. ViewModel(视图模型): 它是View的抽象,或ViewModel之间的中介。它在 View 和 Model 之间传递数据。

3. Model(模型): 它代表来自DAL数据访问层)的数据或领域模型。所以,Model不是DAL,而是引用它。

使用代码

View: 我需要两个窗体(MainFormViewForm)。MainForm 包含一个 TextBox(NameForAdding)和一个 Button(Add)。ListForm 只包含一个 DataGridView。

好的,MainForm 没有代码隐藏。

public partial class MainForm : Form
{
}

如你所知,我使用了一个位于独立程序集中的分部类(MainForm.CodeBehind.cs)来实现它。

public partial class MainForm
{
	//fields
        private ViewModel viewModel;

        //Constructor
        public MainForm()
        {
            InitializeComponent();
            this.InitialControlHandlers();
        }                

        //Defining all Control handler methods       
        private void InitialControlHandlers()
        {
            this.viewModel = new ViewModel();        
            ListForm listForm = new ListForm()
            {
                ViewModel = this.viewModel
            };
            
            //Defines the listForm as an owned form
            this.AddOwnedForm(listForm);            

            //Defines: if ListForm closes, the MainForm will close too
            listForm.FormClosing+=new FormClosingEventHandler(
                (object sender, FormClosingEventArgs e) =>
                {
                    listForm.Dispose();
                    this.Close();
                });           

            //If the end user press Enter key, it will be like (s)he clicked on Add button. 
            this.NameForAdding.KeyPress += new KeyPressEventHandler(
                (object sender, KeyPressEventArgs e) =>
                {
                    if (e.KeyChar == (char)13)
                        this.AddToList();
                });

            this.Add.Click += new EventHandler(
                (object sender, EventArgs e) =>
                {
                    AddToList();
                }
                );

            //Defining a command for this button. It will be un-boxed to execute.
            this.Add.Tag = viewModel.AddToListCommand;

            //Defining a command for TextChanged. It will be un-boxed to execute.
            this.NameForAdding.Tag = viewModel.TextChangedCommand;

            //Binding the Text property of ViewModel to TextBox
            BindingSource binding = new BindingSource();            
            binding.DataSource = viewModel.Text;
            this.NameForAdding.DataBindings.Add(new Binding("Text", viewModel, "Text"));

            //Showing ListForm at finishing  the form load.
            listForm.Show();
        }
        private void AddToList()
        {
	    viewModel.Execute(this.Add.Tag, null);

            //After passing the value of the TextBox, it will be empty.
            this.NameForAdding.Text = string.Empty;

            this.NameForAdding.Focus();
        }
}

窗体的所有事件、方法等都定义在分部类中。

ViewModel 

public class ViewModel : INotifyPropertyChanged
{
        //Properties
        public string Text { get; set; }

        private ICommand _addToListCommand;
        public ICommand AddToListCommand
        {
            get
            {
                if (_addToListCommand == null)
                    _addToListCommand = new AddToList(this);
                return _addToListCommand;
            }
        }

        private ICommand _textChangedCommand;
        public ICommand TextChangedCommand
        {
            get
            {
                if (_textChangedCommand == null)
                    _textChangedCommand = new TextChanged(this);
                return _textChangedCommand;
            }
        }

        private ObservableCollection<StringValue> _nameList;
        public ObservableCollection<StringValue> NameList
        {
            get
            {
                return _nameList;
            }

            set
            {
                _nameList = value;
                _nameList.CollectionChanged += 
                        new System.Collections.Specialized.NotifyCollectionChangedEventHandler
                                                   (MyProperty_CollectionChanged);
            }
        } 

        //PropeetyChanged Handlers
        void model_PropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            this.NameList = 
                  (ObservableCollection<StringValue>)sender; 
                               //For Geting any new Changes in Entity 
        }

        void MyProperty_CollectionChanged
               (object sender, 
                      System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
        {
            {
                NotifyPropertyChanged("NameList");
            }
        }

        public void NotifyPropertyChanged(string propertyName)
        {
            if (this.PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }        

        //INotifyPropertyChanged Members
        public event PropertyChangedEventHandler PropertyChanged;        

        //Methods
        public void Execute(object sender, object parameter)
        {
            ((ICommand)sender).Execute(parameter);
        }

        //...
        //For more information about the complete code, download the Source Code 
 

以及它的构造函数:

public ViewModel()
{
      Model model = new Model();
      model.PropertyChanged += new PropertyChangedEventHandler(model_PropertyChanged);
      //...
}

以及 2 个将在 UI 和 ViewModel 中使用的命令类。

public class AddToList : ICommand
{
    //Constructor
    public AddToList(ViewModel viewModel)
    {
        this.ViewModel = viewModel;
    }

    //Properties
    public ViewModel ViewModel { get; set; }

    //ICommand Members
    public void Execute(object sender)
    {
        if (string.IsNullOrEmpty(this.ViewModel.Text))
            return;
        if (string.IsNullOrEmpty(this.ViewModel.Text.Trim()))
            return;
        this.ViewModel.NameList.Add(new StringValue(this.ViewModel.Text));
    }    
}

public class TextChanged : ICommand
{
    //Constructor
    public TextChanged(ViewModel viewModel)
    {
        this.DataAccess = viewModel;
    }

    //Properties
    public ViewModel DataAccess { get; set; }

    //ICommand Members
    public void Execute(object sender)
    {
        this.DataAccess.Text = sender.ToString();
    }    
}

模型

public class Model:INotifyPropertyChanged
{
   public ObservableCollection<StringValue> List = new ObservableCollection<StringValue>();

   private DataAccess dal = new DataAccess();

   public Model()
   {
      this.List = dal.Select();

      dal.DatabaseUpdated += new DataAccess.UpdateHandler(UpdataFromDal);

      this.List.CollectionChanged += 
                      new System.Collections.Specialized.NotifyCollectionChangedEventHandler
                                                                       (List_CollectionChanged);
   }

   void List_CollectionChanged(object sender, 
                            System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
   {
      dal.Update(this.List);
   }

   private void UpdataFromDal(ObservableCollection<StringValue> list)
   {
      this.List = list;

      List.CollectionChanged += 
                      new System.Collections.Specialized.NotifyCollectionChangedEventHandler
                                                                       (List_CollectionChanged);

      if (PropertyChanged != null)
         PropertyChanged(List, null);
   }

   #region INotifyPropertyChanged Members

      public event PropertyChangedEventHandler PropertyChanged;

   #endregion
}

好的,那么 ListForm 呢?ListForm 有一个 DataGridView 和一个作为 ViewModel 的属性。
我将 DataGridView 绑定到 ViewModel.NameList

private void BindDataGridView()
{
   BindingSource binding = new BindingSource();
   binding.DataSource = ViewModel.NameList;
   this.DataGridView.DataSource = binding;
}

void ViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
   BindDataGridView(); 
}

在窗体的 Load 事件中:

ViewModel.PropertyChanged += new PropertyChangedEventHandler(ViewModel_PropertyChanged);

概述说明

如果我想使用“倒金字塔”来说明。

  • 在示例中,最终用户可以在主窗体(MainForm)中多次输入一个名字,并在另一个窗体(ListForm)的GridView中看到添加的名字。
  • 最终用户还会看到 ListForm 中一些他/她从未添加过的新名字,就像其他用户正在更新列表一样(仅为模拟)。
  • 在“最终用户”的视角下,添加过程是:1. 填写 TextBox。2. 点击 Add(按钮)。3. 查看 ListForm 中的结果。
  • 幕后:当TextBoxNameForAdding)发生变化时,ViewModelText 属性会发生变化(由于绑定),当ButtonAdd)被点击时,一个命令将被触发,将 Text 添加到 ModelList 中。之后,Model 将使用 DataAccess 将其 List 与数据库同步。如果 List 中有任何更改,ViewModelNameList 将会更新,并且 NameList 的任何更新都会重新绑定 ListForm 中的GridView
  • WinForm 不支持双向绑定,所以我实现了 INotifyPropertyChanged 并使用了 NotifyCollectionChangedEventHandler

Sequence Diagram

解答疑点

StringValue 是什么?一个字符串对象只有一个公共属性(Length),如果你将一个字符串集合绑定到 DataGridView,它将只显示长度。StringValue 是一个自定义类,我创建了一个 StringValue 集合来显示在 DataGirdView 中。

public class StringValue
{
   public string Value { get; set; }
   public StringValue(string value)
   {
      this.Value = value;
   }
}

ICommand 是什么?这是我创建的一个接口,模仿了WPF内置的ICommand接口。

public interface ICommand
{
   void Execute(object sender);
}

我在控件的 Tag 中设置了命令(在分部类中)。

this.Add.Tag = viewModel.AddToListCommand;
this.NameForAdding.Tag = viewModel.TextChangedCommand;

当要执行命令时,标签会被解装箱。

public void Execute(object sender, object parameter)
{
   ((ICommand)sender).Execute(parameter);
}

为什么我没有充分阐述 ViewModel 类?因为它几乎就像WPF中的View-Model,并且有足够多的网站、书籍、文章等可供参考。(由于不及时,这篇文章会太长)。

为什么 Model 看起来有点奇怪?Model 使用 DataAcess 类来模拟与数据库的交互(但它实际上没有)。关于 Model 的一切就是,它是表示层DAL之间的网关,同时,它实现了 INotifyPropertyChanged 以通知 ViewModel 实体更新。

DataAccess 做什么?DataAccess 类不属于该模式的一部分,我创建它是为了模拟数据库访问。它还生成一些随机名称来模拟并发更新。(请下载演示和源代码。)

还有什么吗?
请帮忙!如果你在这篇文章中遇到任何模糊之处,请通知我,不要犹豫提问。这有助于我改进文章。

谢谢。

使用 C# 的 Windows Form 应用程序的 MVVM(模型-视图-视图模型)模式 - CodeProject - 代码之家
© . All rights reserved.