C#/WinForms 中的模型-视图-控制器 (MVC) 模式






4.94/5 (126投票s)
简明扼要地介绍 MVC 的实现,不进行冗长的讨论或细节的阐述。
引言
本文档用于演示在 .NET 中使用 C#/WinForm 实现 MVC 模式。
这里是一个简单的“用户信息管理器”应用程序,它按照模型-视图-控制器 (MVC) 模式进行组织。
该应用程序显示联系人列表,并允许您添加、修改和删除现有联系人。核心思想是将用户界面分离为视图 (View)(负责创建显示,并在需要时调用模型获取信息)和控制器 (Controller)(负责响应用户请求,并在需要时与视图和模型进行交互)。MVC 模式的主要优势是松散耦合。所有层都与其自身的功能分离。更换某个层与其他类型的层非常容易。换句话说,MVC 模式是将 UI 行为分解为独立的部分,以提高重用性和可测试性。我使用 Visual Studio 2010 Ultimate 和 .NET 4.0 来创建此应用程序。
背景
顾名思义,模型-视图-控制器 (Model-View-Controller) 考虑了三个部分:
- 模型 (Model): 应负责应用程序域的数据。
- 视图 (View): 在用户界面中展示模型的外观。
- 控制器 (Controller): 它是 MVC 的核心,是连接模型和视图的中间人,即它接收用户输入,操作模型并促使视图更新。

解决方案
用户信息管理器是一个可以存储客户联系信息的应用程序。该应用程序显示联系人列表,并允许您添加、修改和删除现有联系人。所有客户都具有 ID、名字、姓氏和性别。操作员维护客户列表的屏幕可能看起来像这样:
可以查看、添加、删除和更新客户列表(目前仅包含 V.I.P. 会员,如果您想成为俱乐部会员,请随时询问 " src="https://codeproject.org.cn/script/Forums/Images/smiley_smile.gif" /> .... 没问题,免费的)。添加新用户后,其 ID 不能再更改。
类图
在系统设计中,会识别出许多类并将它们分组到类图中,这有助于确定对象之间的关系。
组件说明
控制器部分
为了将逻辑从视图中分离出来,我们必须使视图尽可能“无能”,因此我们倾向于让控制器完成所有繁重的工作,只向视图传递一些不需要进一步处理的简单命令。根据我们的设计,我们通过定义一个 `IUsersView` 接口来实现这一点,视图必须实现该接口。此接口仅包含我们需要的属性/方法的签名。
using System;
using WinFormMVC.Model;
namespace WinFormMVC.Controller
{
public interface IUsersView
{
void SetController(UsersController controller);
void ClearGrid();
void AddUserToGrid(User user);
void UpdateGridWithChangedUser(User user);
void RemoveUserFromGrid(User user);
string GetIdOfSelectedUserInGrid();
void SetSelectedUserInGrid(User user);
string FirstName { get; set; }
string LastName { get; set; }
string ID { get; set; }
string Department { get; set; }
User.SexOfPerson Sex { get; set; }
bool CanModifyID { set; }
}
}
现在我们有了一个相当不错的接口,包含许多方法。即使 MVC 模式正式声明控制器应接收事件并对视图做出反应,但通常更实用、更容易让视图订阅事件,然后将处理委托给控制器。
最后,我将展示控制器的实际实现(请参阅 `UsersController` 类)。它将模型(`User` 类)与视图(`UserView` 类)连接起来。
public class UsersController
{
//Notice we only use the interfaces. This makes the test more
//robust to changes in the system.
IUsersView _view;
IList _users;
User _selectedUser;
//The UsersController depends on abstractions(interfaces).
//It's easier than ever to change the behavior of a concrete class.
//Instead of creating concrete objects in UsersController class,
//we pass the objects to the constructor of UsersController
public UsersController(IUsersView view, IList users)
{
_view = view;
_users = users;
view.SetController(this);
}
public IList Users
{
get { return ArrayList.ReadOnly(_users); }
}
private void updateViewDetailValues(User usr)
{
_view.FirstName = usr.FirstName;
_view.LastName = usr.LastName;
_view.ID = usr.ID;
_view.Department = usr.Department;
_view.Sex = usr.Sex;
}
private void updateUserWithViewValues(User usr)
{
usr.FirstName = _view.FirstName;
usr.LastName = _view.LastName;
usr.ID = _view.ID;
usr.Department = _view.Department;
usr.Sex = _view.Sex;
}
public void LoadView()
{
_view.ClearGrid();
foreach (User usr in _users)
_view.AddUserToGrid(usr);
_view.SetSelectedUserInGrid((User)_users[0]);
}
public void SelectedUserChanged(string selectedUserId)
{
foreach (User usr in this._users)
{
if (usr.ID == selectedUserId)
{
_selectedUser = usr;
updateViewDetailValues(usr);
_view.SetSelectedUserInGrid(usr);
this._view.CanModifyID = false;
break;
}
}
}
public void AddNewUser()
{
_selectedUser = new User("" /*firstname*/,
"" /*lastname*/,
"" /*id*/,
""/*department*/,
User.SexOfPerson.Male/*sex*/);
this.updateViewDetailValues(_selectedUser);
this._view.CanModifyID = true;
}
public void RemoveUser()
{
string id = this._view.GetIdOfSelectedUserInGrid();
User userToRemove = null;
if (id != "")
{
foreach (User usr in this._users)
{
if (usr.ID == id)
{
userToRemove = usr;
break;
}
}
if (userToRemove != null)
{
int newSelectedIndex = this._users.IndexOf(userToRemove);
this._users.Remove(userToRemove);
this._view.RemoveUserFromGrid(userToRemove);
if (newSelectedIndex > -1 && newSelectedIndex < _users.Count)
{
this._view.SetSelectedUserInGrid((User)_users[newSelectedIndex]);
}
}
}
}
public void Save()
{
updateUserWithViewValues(_selectedUser);
if (!this._users.Contains(_selectedUser))
{
//Add new user
this._users.Add(_selectedUser);
this._view.AddUserToGrid(_selectedUser);
}
else
{
//Update existing user
this._view.UpdateGridWithChangedUser(_selectedUser);
}
_view.SetSelectedUserInGrid(_selectedUser);
this._view.CanModifyID = false;
}
}
控制器类对应用程序非常重要且至关重要。保持其轻量、敏捷并与其他程序组件松散耦合非常重要。
视图部分
本节将重点介绍加载具有用户列表的视图的场景。
如前所述,我们的视图必须实现 `IUsersView` 接口。以下代码显示了实现的一部分。
namespace WinFormMVC.View
{
public partial class UsersView : Form, IUsersView
{
`UsersView` 的 `SetController()` 成员函数允许我们告知视图它必须将事件转发给哪个控制器实例,并且所有事件处理程序都会在控制器上调用相应的“事件”方法。如您所见,`UsersView` 也依赖于抽象...
public void SetController(UsersController controller)
{
_controller = controller;
}
我们还使用 `IUsersView` 接口的几个方法的实现,这些方法使用 `User` 对象。
public void AddUserToGrid(User usr)
{
ListViewItem parent;
parent = this.grdUsers.Items.Add(usr.ID);
parent.SubItems.Add(usr.FirstName);
parent.SubItems.Add(usr.LastName);
parent.SubItems.Add(usr.Department);
parent.SubItems.Add(Enum.GetName(typeof(User.SexOfPerson), usr.Sex));
}
public void UpdateGridWithChangedUser(User usr)
{
ListViewItem rowToUpdate = null;
foreach (ListViewItem row in this.grdUsers.Items)
{
if (row.Text == usr.ID)
{
rowToUpdate = row;
}
}
if (rowToUpdate != null)
{
rowToUpdate.Text = usr.ID;
rowToUpdate.SubItems[1].Text = usr.FirstName;
rowToUpdate.SubItems[2].Text = usr.LastName;
rowToUpdate.SubItems[3].Text = usr.Department;
rowToUpdate.SubItems[4].Text = Enum.GetName(typeof(User.SexOfPerson), usr.Sex);
}
}
public void RemoveUserFromGrid(User usr)
{
ListViewItem rowToRemove = null;
foreach (ListViewItem row in this.grdUsers.Items)
{
if (row.Text == usr.ID)
{
rowToRemove = row;
}
}
if (rowToRemove != null)
{
this.grdUsers.Items.Remove(rowToRemove);
this.grdUsers.Focus();
}
}
public string GetIdOfSelectedUserInGrid()
{
if (this.grdUsers.SelectedItems.Count > 0)
return this.grdUsers.SelectedItems[0].Text;
else
return "";
}
public void SetSelectedUserInGrid(User usr)
{
foreach (ListViewItem row in this.grdUsers.Items)
{
if (row.Text == usr.ID)
{
row.Selected = true;
}
}
}
public string FirstName
{
get { return this.txtFirstName.Text; }
set { this.txtFirstName.Text = value; }
}
public string LastName
{
get { return this.txtLastName.Text; }
set { this.txtLastName.Text = value; }
}
public string ID
{
get { return this.txtID.Text; }
set { this.txtID.Text = value; }
}
public string Department
{
get { return this.txtDepartment.Text; }
set { this.txtDepartment.Text = value; }
}
public User.SexOfPerson Sex
{
get
{
if (this.rdMale.Checked)
return User.SexOfPerson.Male;
else
return User.SexOfPerson.Female;
}
set
{
if (value == User.SexOfPerson.Male)
this.rdMale.Checked = true;
else
this.rdFamele.Checked = true;
}
}
public bool CanModifyID
{
set { this.txtID.Enabled = value; }
}
...
}
模型部分
这个 `User` 类是一个模型类。在此示例中,`User` 是一个非常简单的域类,没有任何行为,而在实际的领域模型中,域类可能包含更多的功能。模型独立于用户界面。它不知道它是被用于文本界面、图形界面还是 Web 界面。模型仅以结构化格式保存内存状态。如您所见,该类仅包含私有数据成员和客户端代码可用的公共接口(属性)。
using System;
namespace WinFormMVC.Model
{
public class User
{
public enum SexOfPerson
{
Male = 1,
Female = 2
}
private string _FirstName;
public string FirstName
{
get { return _FirstName; }
set
{
if (value.Length > 50)
Console.WriteLine("Error! FirstName must be less than 51 characters!");
else
_FirstName = value;
}
}
private string _LastName;
public string LastName
{
get { return _LastName; }
set
{
if (value.Length > 50)
Console.WriteLine("Error! LastName must be less than 51 characters!");
else
_LastName = value;
}
}
private string _ID;
public string ID
{
get { return _ID; }
set
{
if (value.Length > 9)
Console.WriteLine("Error! ID must be less than 10 characters!");
else
_ID = value;
}
}
private string _Department;
public string Department
{
get { return _Department; }
set { _Department = value; }
}
private SexOfPerson _Sex;
public SexOfPerson Sex
{
get { return _Sex; }
set { _Sex = value; }
}
public User(string firstname, string lastname, string id, string department, SexOfPerson sex)
{
FirstName = firstname;
LastName = lastname;
ID = id;
Department = department;
Sex = sex;
}
}
}
客户端部分
现在是时候展示我们如何有效地使用 MVC 范例了,即我们 MVC 架构的代码组件(请参阅 *UseMVCApplication.csproj*)。
using System.Collections;
using WinFormMVC.Model;
using WinFormMVC.View;
using WinFormMVC.Controller;
namespace UseMVCApplication
{
static class Program
{
/// The main entry point for the application.
[STAThread]
static void Main()
{
//Here we are creating a View
UsersView view = new UsersView();
view.Visible = false;
//Here we are creating a list of users
IList users = new ArrayList();
//Here we are add our "commoners" in the list of users
users.Add(new User("Vladimir", "Putin",
"122", "Government of Russia",
User.SexOfPerson.Male));
users.Add(new User("Barack", "Obama",
"123", "Government of USA",
User.SexOfPerson.Male));
users.Add(new User("Stephen", "Harper",
"124", "Government of Canada",
User.SexOfPerson.Male));
users.Add(new User("Jean", "Charest",
"125", "Government of Quebec",
User.SexOfPerson.Male));
users.Add(new User("David", "Cameron",
"126", "Government of United Kingdom",
User.SexOfPerson.Male));
users.Add(new User("Angela", "Merkel",
"127", "Government of Germany",
User.SexOfPerson.Female));
users.Add(new User("Nikolas", "Sarkozy",\
"128", "Government of France",
User.SexOfPerson.Male));
users.Add(new User("Silvio", "Berlusconi",
"129", "Government of Italy",
User.SexOfPerson.Male));
users.Add(new User("Yoshihiko", "Noda",
"130", "Government of Japan",
User.SexOfPerson.Male));
//Here we are creating a Controller and passing two
//parameters: View and list of users (models)
UsersController controller = new UsersController(view, users);
controller.LoadView();
view.ShowDialog();
}
}
}
为什么它好?
- 使用 MVC 模式的主要优点是它使用户界面的代码更具可测试性。
- 它为用户界面设计过程提供了一种非常结构化的方法,这本身就有助于编写干净、可测试的代码,并且易于维护和扩展。
模型-视图-控制器 (Model-View-Controller) 是一个经过充分验证的设计模式,用于解决分离数据(模型)和用户界面(视图)关注点的问题,从而使用户界面的更改不会影响数据处理,并且可以在不影响/更改 UI 的情况下更改数据。MVC 通过引入一个中间组件:控制器,来实现数据访问和业务逻辑层与 UI 和用户交互的分离。这种 MVC 架构使得在灵活的程序设计中创建可重用组件成为可能(组件可以轻松修改)。
历史
- 2012 年 5 月 10 日:初次发布