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






4.84/5 (61投票s)
它是一种类似于 MVVM 的模式,但适用于 WinForms。
引言
MVVM 模式因其优点而受到 WPF/Silverlight 开发者的青睐。微软表示,MVVM
- 将应用程序的业务逻辑和表示逻辑与其UI分开,使其更容易测试、维护和演进。
- 允许开发人员和UI设计人员在开发各自的应用程序部分时进行更轻松的协作。(请查看链接)
MVVM 是为 WPF 设计的,但有没有办法让 WinForms 也拥有这些优点呢?答案是肯定的(好消息,也许也不算太新)。Windows Form 应用程序也可以拥有这些优点,当然,这并不意味着完全是Model-View-ViewModel(MVVM)模式。我搜索并找到许多方法(Google 搜索),其中一些很棒,有些则不太理想(!)。但我试图提供一个简单实用的解决方案。希望你会感兴趣。
背景
正如你所知,程序逻辑和内容在ASP.Net中是分开的。在WPF中,程序逻辑也与UI分开。在 Windows Forms 中,在我接触到MVVM模式之前,我倾向于使用分部类将程序逻辑与UI分开。我对在WPF中使用该模式很感兴趣,因此我决定为WinForm也找到类似的方法,但我一直太忙,没有时间去研究它。有一天,我尝试将MVVM应用到WinForm中,这是我在业余时间研究该主题 3 小时的结果。
一个非常快速简短的 MVVM 回顾
MVVM 意为 Model-View-ViewModel(模型-视图-视图模型)
1. View(视图): 指的是UI显示的所有元素。有些人说 View 是UI,另一些人说 View 是UI“但包含”逻辑(我喜欢后一种说法)。此外,它向 ViewModel 传递命令。
2. ViewModel(视图模型): 它是View的抽象,或View和Model之间的中介。它在 View 和 Model 之间传递数据。
3. Model(模型): 它代表来自DAL(数据访问层)的数据或领域模型。所以,Model不是DAL,而是引用它。
使用代码
View: 我需要两个窗体(MainForm
和 ViewForm
)。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
中的结果。 - 幕后:当TextBox(
NameForAdding
)发生变化时,ViewModel
的Text
属性会发生变化(由于绑定),当Button(Add
)被点击时,一个命令将被触发,将Text
添加到Model
的List
中。之后,Model
将使用DataAccess
将其List
与数据库同步。如果List
中有任何更改,ViewModel
的NameList
将会更新,并且NameList
的任何更新都会重新绑定ListForm
中的GridView。 - WinForm 不支持双向绑定,所以我实现了
INotifyPropertyChanged
并使用了NotifyCollectionChangedEventHandler
。
解答疑点
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
类不属于该模式的一部分,我创建它是为了模拟数据库访问。它还生成一些随机名称来模拟并发更新。(请下载演示和源代码。)
还有什么吗?
请帮忙!如果你在这篇文章中遇到任何模糊之处,请通知我,不要犹豫提问。这有助于我改进文章。
谢谢。