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

MVP-VM (Model View Presenter - ViewModel) 结合 Windows Forms 应用程序中的数据绑定和 BackgroundWorker

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.98/5 (29投票s)

2010年6月17日

CPOL

19分钟阅读

viewsIcon

140349

downloadIcon

4967

本文介绍了一种创建 Windows Forms 应用的方法,使 Form.cs 文件尽可能精简。

引言

您的 XxxForm.cs.vb 文件是否充斥着大量处理业务逻辑的事件处理程序?即使您能够将领域逻辑与 UI 分离,您的 Form 类文件是否仍然充斥着与用户输入验证相关的代码,或者用于使您的应用更加用户友好的控件的 Enable/Disable 或 Show/Hide 代码?如果是这样,本文可以帮助您整理它们,并且您可能会将 90% 的内容从表单文件中分离出来。实现这一点的一些关键词是数据绑定和 MVP-VM(Model View Presenter – ViewModel)模式。

上面的截图是我将在本文中使用的示例应用程序。由于我厌倦了使用“Employee”、“Customer”或“Order”类的 OOP 教程,我想做一些完全不同的事情,并决定使用我从 Project Euler 网站上获取的一个数学问题 - http://projecteuler.net/。它以通用形式解决了问题编号 1:“求一千以内所有 3 或 5 的倍数之和”。截图显示了正确答案,您可以通过填写两个文本框并单击“查找”按钮来获取它。

背景

当我最初在 Windows 平台编程时,微软使用的术语是“Document-View-Controller”。如今,它被称为 Model View Controller。Martin Fowler 的文章,如“GUI Architecture” - https://martinfowler.com.cn/eaaDev/uiArchs.html 和“Separated Presentation” - https://martinfowler.com.cn/eaaDev/SeparatedPresentation.html 详细解释了 MVC 和其他相关架构,如 MVP(Model View Presenter)及其后代。John Gossman 在他的博客 http://blogs.msdn.microsoft.com/johngossman/archive/2005/10/08/478683.aspx 中创造了 MVVM 这个词,我强烈推荐阅读。尽管 MVVM 是为具有数据绑定的 WPF/Silverlight 应用设计的,但我们应该能够在 Windows Forms 应用中使用这个思想。Aviad Ezra 在他的博客 http://aviadezra.blogspot.com/2009/08/mvp-mvvm-winforms-data-binding.html 中写了关于这个的内容,他将 MVP-VM 定义为“WPF MVVM 的 Windows Forms (WinForms) 等价物。MVP-VM (Model View Presenter – Model View) 模式是为需要完全测试覆盖率并广泛使用数据绑定来同步表示与域模型的 WinForms 应用程序量身定制的解决方案。”

我更侧重于如何使表单类文件尽可能精简,而不是通过使用 MVP-VM 模式来寻求完全的测试覆盖能力。事实上,示例应用程序的 form.cs 文件只有 87 行 C# 代码,包含了任何产品质量代码所需的内置验证和错误处理。代码 FxCop 清洁且 StyleCop 清洁,除了文档规则。我还使用了 .NET 4 新引入的 Code Contracts - http://msdn.microsoft.com/en-us/devlabs/dd491992.aspx,而不是使用更传统的 Debug.Assert() 或 If-argument-is-invalid-then-throw-ArgumentException 风格的错误检查。你可能会问为什么。这完全是为了代码的可读性。我们将在代码中遇到它们时进行解释。不过,它还不是 Visual Studio 2010 的一部分。因此,我提供了两个下载版本,一个包含 Code Contracts Rewriter,一个不包含,这样那些在 VS2010 中没有它的人仍然可以编译和运行该软件。

软件需求规格

首先,让我们确保这个简单应用程序的需求。无论程序大小如何,编写 SRS(软件需求规格)或用户故事(取决于您项目采用的 SDLC(软件开发生命周期)模型)都应该是程序员的第二天性。

  1. 软件应以通用形式解决 Project Euler 问题编号 1。
  2. 用户应能够为“种子”提供任意数量的正 32 位整数。
  3. 用户应能够指定 32 位整数范围内的上限。
  4. 软件应记住用户启动应用程序时的最后设置和答案。
  5. 软件应显示解决给定问题的进度。
  6. 用户应能够在解决过程中取消该过程。
  7. 软件应使用 Windows Forms 技术作为设计约束。

解决方案 - 模型(业务领域)

我设计了一个单独的域类来解决给定问题,并将其命名为 Solver。它有一个用于搜索限制的 Max 属性,一个可以放入任意数量 Seeds 的整数存储桶,以及一个名为 FindSumOfMultiplesOfSeedsBelowMax() 的方法,这是该应用程序的核心。除了这三个公共成员外,我还添加了一个名为 Answer 的属性来获取结果。您可以轻松想象如何使用这个类 - .Max = 1000.Seeds.Add(3).Seeds.Add(5),调用方法,然后通过 .Answer 获取结果。生成的类规范如下

看起来一个不错、简单的 API,不是吗?MaxAnswer 都是整数属性,存储桶名为 Seeds。我将存储桶类类型命名为 PositiveIntegerSetCollection,因为它应该只接受正整数。存储桶应该是一个数学“集合”而不是简单的整数集合,因为它应该忽略重复的元素。无论如何,我创建了这个规范,并能够将其交给项目的另一位成员,理论上是这样。不幸的是,由于我是项目的唯一成员,所以我接受了这个角色。这里重要的是,您最好专注于业务逻辑,不要被显示进度或检索/保存设置和答案等次要需求所分心。那些是 UI 的东西。它们不应该影响您解决问题的方式。

如果这是一个像 C# Fundamentals 这样的课程项目,您只需提交这个类的实现就可以。然而,作为真实世界的、产品质量的软件,这仅仅是开始。您需要添加 UI 并满足 SRS 的所有验证和错误处理要求,同时不损失 UI 的响应能力。事实证明,Solver 类占据了整个代码(不包括 Designer.cs 文件)的 10% 多一点。90% 的代码需要在别处使用。

解决方案 - UI(又名 View)

对于 UI,我决定使用两个文本框来接受 MaxSeeds,一个标签来显示 Answer,一个按钮来开始计算,另一个按钮在计算过程中取消它,如本文开头所示。一些用户可能想要一个不同的 UI;例如,一个用于单个种子的文本框,“添加”按钮将种子添加到列表框,以及一个“删除”按钮来删除列表中选定的种子。这表明 UI 可能非常脆弱,这正是为什么我们在设计业务域类时,不应(过度)考虑 UI 问题的原因。否则,UI 的更改将轻易传播到域类,您将不得不修改并重新测试它们。

为了显示进度,我在“取消”按钮旁边放置了一个 ProgressBar。我选择在软件解决问题时才显示它,因为当它不解决问题时,从用户的角度来看,它只是一个干扰。我还选择在软件解决问题时禁用按钮和文本框,以阻止用户操作。另一方面,“取消”按钮只能在软件解决问题时启用。因此,软件需要控制进度条的 Visible 属性以及按钮和文本框的 Enabled 属性。为了在文本框中显示用户输入错误,我决定使用 ErrorProvider,而不是在模态对话框中报告错误。

如我所说,表单类只有 87 行代码,如下所示

using System;
using System.ComponentModel;
using System.Windows.Forms;

namespace DataBinding
{
    public partial class SolverForm : Form
    {
        private readonly SolverPresenter presenter;

        public SolverForm()
        {
            InitializeComponent();
        }

        public SolverForm(SolverPresenter presenter) : this()
        {
            this.presenter = presenter;
            this.presenter.FindAsyncCompleted += Presenter_FindAsyncCompleted;
            solverViewModelBindingSource.DataSource = this.presenter.SolverViewModel;

            // Setting the error provider's DataSource needs to be done *AFTER*
            // setting the binding source's DataSource. Otherwise, binding to the
            // Visible property wouldn't work (always invisible).
            // See details on http://social.msdn.microsoft.com/Forums/en-US/
            //      winformsdatacontrols/thread/269e0803-4e05-462e-91b7-abd768615f68/
            //      #f0ac03e5-eb18-4abf-a36c-619aeea13382
            errorProvider1.DataSource = solverViewModelBindingSource;
        }

        private static void Presenter_FindAsyncCompleted(object sender, 
                            AsyncCompletedEventArgs e)
        {
            try
            {
                if (e.Error != null)
                {
                    throw e.Error;
                }
            }
            catch (OperationFailedException ex)
            {
                MessageBox.Show(
                    ex.Message,
                    Application.ProductName,
                    MessageBoxButtons.OK,
                    MessageBoxIcon.None,
                    MessageBoxDefaultButton.Button1,
                    MessageBoxOptions.DefaultDesktopOnly);
            }
        }

        private void FindButton_Click(object sender, EventArgs e)
        {
            try
            {
                this.presenter.FindClicked();
            }
            catch (OperationFailedException ex)
            {
                MessageBox.Show(
                    ex.Message,
                    Application.ProductName,
                    MessageBoxButtons.OK,
                    MessageBoxIcon.None,
                    MessageBoxDefaultButton.Button1,
                    MessageBoxOptions.DefaultDesktopOnly);
            }
        }

        private void CancelButton_Click(object sender, EventArgs e)
        {
            try
            {
                this.presenter.CancelClicked();
            }
            catch (OperationFailedException ex)
            {
                MessageBox.Show(
                    ex.Message,
                    Application.ProductName,
                    MessageBoxButtons.OK,
                    MessageBoxIcon.None,
                    MessageBoxDefaultButton.Button1,
                    MessageBoxOptions.DefaultDesktopOnly);
            }
        }
    }
}

请注意,该类只有五个方法 - 两个构造函数,两个按钮点击事件处理程序,以及另一个事件处理程序,用于显示软件在尝试解决给定问题时可能捕获的错误消息。第一个构造函数是默认构造函数,没有什么特别之处。第二个构造函数有四行代码,将 ViewModel、BindingSourceErrorProvider 绑定在一起,并设置 Presenter 的完成事件处理程序。Program.cs 中的 Main() 调用此构造函数。当然,form.designer.cs 文件包含在数据源方面将视觉控件的属性实际绑定到相应 ViewModel 属性的语句。要为项目添加数据源,请先构建项目,然后选择 VS2010 上的 <Data/Add New Data Source…> 菜单,在向导的第一页选择“Object”,然后选择 SolverViewModel。Windows Forms Designer 将自动创建一个 solverViewModelBindingSource 并将其放在组件托盘上。如果您不知道如何在 IDE 中将视觉控件的属性绑定到 BindingSource,请参阅 http://msdn.microsoft.com/en-us/library/sw223a62.aspx

按钮点击事件处理程序仅将用户命令委托给 Presenter。Form 类不需要了解业务的任何信息。当然,Form 类中的每个事件处理程序都需要捕获特定的异常。否则,它很容易崩溃。我为此创建了一个自定义的 OperationFailedException。我们不需要深入了解 Presenter 抛出哪些异常;每个类的每个函数都被设计为在“未能按名称执行其功能”时抛出 OperationFailedException [CLARK]。UI 的事件处理程序唯一需要做的是捕获它并在消息框中显示错误消息。除了显示消息之外的所有错误处理都必须在 UI 界面下方的相应位置进行。

FxCop 中有一条黄金法则;不要通过捕获非特异性异常(如 System.ExceptionSystem.SystemException 等)来吞噬错误 [CWALINA/ABRAMS]。因此,请始终只捕获您知道如何处理的特异性异常。

解决方案 - ViewModel

完成业务域和 UI 的设计后,现在是时候考虑模式的 ViewModel 部分了。ViewModel 对象通过 BindingSource 绑定到视觉元素。由于我们使用文本框和标签,因此 MaxSeedsAnswer 属性必须是 string 类型,而不是 integer 或整数存储桶。尽管您可以直接将整数属性绑定到文本框的 Text 属性,但我**不**建议这样做,因为然后字符串到整数的解析在绑定管道的某个地方完成,而您无法控制当字符串无法正确转换为整数时需要显示给用户的错误消息。如果这样做,框架会抛出含糊不清的“Input String was not in a Correct Format”(输入字符串格式不正确),这可能是终端用户无法理解的。不幸的是,在当前的 .NET Framework 中没有办法覆盖此消息。因此,这里的教训是创建一个具有 string 属性的 ViewModel 类,这些属性绑定到 UI 的文本属性,并在您的代码中处理解析/格式化,这样您就可以完全控制字符串如何转换为数值以及反之亦然。

顺便说一句,在我决定在下一个项目中使用 MVP-VM 模式之前,我个人从未真正使用过数据绑定。毕竟,“自 Visual Basic 2.0 起,我们是否就被告知不应该使用绑定?它包含了不可扩展的模式,没有使用良好的编程实践,而且经常不按预期工作。” [KURATA]。然而,微软似乎终于在 2005 年发布 .NET 2.0 时修复了这些问题(好吧,大部分)。我还强烈建议阅读这本书《Doing Objects in Visual Basic 2005》。事实上,我决定几乎照搬书中所述的 Validation 类。

除了对应于 Model 类属性的三个属性之外,我们还需要 ProgressPercentageProgressBarVisibleControlsEnabledControlsDisabled 属性来显示进度并控制视觉元素。在这七个属性中,MaxSeeds 是我们需要在其 Set 属性调用(在用户离开窗体上的控件时发生)上实现验证的属性。如果存在任何验证错误,我们希望借助 ErrorProvider 显示它们。这是通过 IDataErrorInfo 接口完成的。对于其余属性,我们需要“通知”绑定管道的更改,以便视觉控件正确反映更改。这是通过 INotifyPropertyChanged 接口完成的。

适应绑定到相应 UI 属性的属性是 ViewModel 的主要职责。验证用户输入是第二重要的职责。它还负责通过适配接口将已验证的用户输入(Max 字符串和 Seeds 字符串)直接传播到 Model(即 Solver 类)的相应业务属性。第三个职责也很重要,因为业务模型的 API(Max 整数和 Seeds 整数存储桶)几乎总是与 UI(Max 字符串和 Seeds 字符串)不同。总得有人来适配接口。此外,ViewModel 的角色是被动的,因为它不实际执行用户发出的命令。主动角色赋予了 Presenter。

解决方案 - Presenter

现在是时候解释 MVP-VM 模式的最后一个参与者:Presenter。在 WPF 中,支持 ICommand 接口,这样 ViewModel 就可以直接向 View 公开命令。所以,不需要 Presenter。然而,在 Windows Forms 中,数据绑定不支持这一点,所以我们必须自己来实现。这就是 Presenter 的作用 - 实际实现 View 从用户那里收到的命令。在此应用程序中,当用户单击“查找”或“取消”按钮时,View 会调用 FindClicked()CancelClicked() 函数。以下是这些函数中代码

public void FindClicked()
{
    this.SolverViewModel.Answer = string.Empty;
    this.SolverViewModel.Validate();
    if (this.SolverViewModel.IsValid)
    {
        this.SolverViewModel.UpdateModel();
        this.SolverViewModel.ProgressBarVisible = true;
        this.SolverViewModel.ControlsEnabled = false;
        this.solver.FindSumOfMultiplesOfSeedsBelowMaxAsync();
    }
}
public void CancelClicked()
{
    this.solver.CancelAsync();
}

private void Solver_AsyncCompleted(object sender, AsyncCompletedEventArgs e)
{
    this.OnFindAsyncCompleted(e);
    this.SolverViewModel.ProgressPercentage = 0;
    this.SolverViewModel.ProgressBarVisible = false;
    this.SolverViewModel.ControlsEnabled = true;
}

FindClicked() 在此应用程序中扮演关键角色,因为它协调 Model(AsynchronousSolver)和 ViewModel(SolverViewModel)来完成工作。它首先清除 Answer 标签,让 ViewModel 验证用户输入 - Max 字符串和 Seeds 字符串。如果它们有效,它会让 ViewModel 更新 Model 的相应输入属性,显示进度条,禁用相关控件,最后调用 Model 的业务函数 - FindSumOfMultiplesOfSeedsBelowMaxAsync()CancelClicked() 通过调用 CancelAsync() 来尝试取消 FindSumOfMultiplesOfSeedsBelowMaxAsync() 的执行。当 Model 完成工作时 - 无论是因为成功解决了问题并更新了其 Answer 属性,还是因为由于某些错误而抛出了异常 - 它总是在最后引发一个 AsyncCompleted 事件。Presenter 订阅该事件,首先让 UI 通过引发 FindAsyncCompleted 事件来显示任何错误消息,然后清除进度百分比,使进度条不可见,最后再次启用控件以进行下一组输入值。

有些人可能会问 IView 接口在哪里。通常,Presenter 通过 IView 接口与 View 通信,而不是直接与表单类通信,以减少耦合。好吧,我不需要它。在此特定应用程序中,Presenter 不需要访问 View。YAGNI(You Ain't Gonna Need It,你不会需要它)在这里适用。

类图

以上是该应用程序的类图。灰色的是 .NET 类,蓝色的是 UI,米色的是该应用程序特有的业务类,白色的是可以用于其他项目的通用基础类。从图中可以看出,SolverViewModel 最为复杂,因为它关联了五个其他类。因此,我将其放在图的中心。请注意,SolverViewModel 与表单类没有直接关联。

在类图的关联线上添加导航箭头对于识别依赖关系非常有帮助 - 即,当您单元测试类时,您需要为箭头指向的任何类创建模拟对象。例如,以 SolverPresenter 为例。要对其方法进行单元测试,您需要创建两个“模拟”AsynchronousSolverSolverViewModel 的假对象。另一方面,如果一个类没有出向的关联线,它就是相当独立的(没有耦合),并且很容易创建单元测试,因为您不需要任何模拟对象。ProgressSolver 就是此类示例。

为了报告进度,我对 Solver 类进行了一些小的修改。您需要公开一些内部成员来报告内部进程的进度。一种方法是使用继承。因此,FirstArgIsMultipleOfSecondArg() 从私有方法变为受保护方法,并且还被虚拟化以添加报告功能。这实际上暴露了您的“内部细节” - 这在封装方面不是一件好事 - 但显示其内部进程的进度恰恰意味着显示其内部细节。所以,我不得不这样做。派生类(AsynchronousSolver)会覆盖基类的该方法,使其表现略有不同,如下所示

protected override bool FirstArgIsMultipleOfSecondArg(int firstArg, int secondArg)
{
    if (this.progress.IsTimeToReportProgress(1))
    {
        this.backgroundWorker.ReportProgress(this.progress.CurrentPercent);
    }

    if (this.backgroundWorker.CancellationPending)
    {
        throw new OperationCanceledException();
    }        return base.FirstArgIsMultipleOfSecondArg(firstArg, secondArg);
}

首先,检查是否是时候报告进度了。如果是,则调用 BackgroundWorkerReportProgress()。然后,它通过询问 BackgroundWorker 来检查用户是否按下了取消按钮。如果是,它会抛出 OperationCanceledException。最后,它调用基类被覆盖的方法。如您所见,我使用 BackgroundWorker 来保持 UI 的响应能力,同时也允许用户在过程的中间取消操作。当 Max 为 1000 时,计算会立即完成。但是,如果您为 Max 添加五个零,您可能会很高兴能够取消计算。

现在,您可能会想,BackgroundWorker 是在哪里创建的?AsynchronousSolver 不仅覆盖了 FirstArgIsMultipleOfSecondArg(),还添加了一个名为 FindSumOfMultiplesOfSeedsBelowMaxAsync() 的新方法,这是 FindSumOfMultiplesOfSeedsBelowMax() 的异步版本,如下所示

public void FindSumOfMultiplesOfSeedsBelowMaxAsync()
{
    if (this.backgroundWorker != null)
    {
        this.backgroundWorker.Dispose();
    }

    this.backgroundWorker = new BackgroundWorker { WorkerReportsProgress = true, 
                                                   WorkerSupportsCancellation = true };
    this.backgroundWorker.ProgressChanged += this.BackgroundWorker_ProgressChanged;
    this.backgroundWorker.RunWorkerCompleted += this.BackgroundWorker_RunWorkerCompleted;
    this.backgroundWorker.DoWork += this.BackgroundWorker_DoWork;
    this.backgroundWorker.RunWorkerAsync();
}

您可以看到,它创建了一个新的 BackgroundWorker,连接了事件,并在线程池线程中运行任务。Presenter 调用此方法而不是直接调用基类的同步版本。事件处理程序的代码如下

private void BackgroundWorker_DoWork(object sender, DoWorkEventArgs e)
{
    // Any exceptions thrown while running the code below and we don't catch
    // here, will be automatically transferred
    // to RunWorkerCompletedEventArgs.Error property.
    // Because we catch the OperationCanceledException that we are using to
    // indicate that the user canceled the operation,
    // RunWorkerCompletedEventArgs.Error will be null.

    // When an exception is thrown and you are running it from within Visual Studio,
    // VS will pop up an "xxxException was unhandled by user code" message box.
    // Do not be warned, since this is by design. Simply clicking the Run button
    // again will let VS continue running the code. Read the following excerpt.
                // Excerpt from MSDN BackgroundWorker.DoWork Event:
    //   "If the operation raises an exception that your code does not handle,
    //   the BackgroundWorker catches the exception and passes it into the
                //   RunWorkerCompleted event handler, where it is exposed as the Error
    //   property of System.ComponentModel.RunWorkerCompletedEventArgs.
    //   If you are running under the Visual Studio debugger, the debugger
    //   will break at the point in the DoWork event handler where the unhandled
    //   exception was raised."
    try
    {
        this.progress = new Progress(Seeds.Count, Max);
        FindSumOfMultiplesOfSeedsBelowMax();
    }
    catch (OperationCanceledException)
    {
        // e.Cancel will be transferred to RunWorkerCompletedEventArgs.Canceled.
                        e.Cancel = true;
    }
}

private void BackgroundWorker_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
    this.OnProgressChanged(new ProgressChangedEventArgs(e.ProgressPercentage, null));
}

private void BackgroundWorker_RunWorkerCompleted(object sender, 
                              RunWorkerCompletedEventArgs e)
{
    this.OnPropertyChanged(new PropertyChangedEventArgs(
                           SolverViewModel.AnswerPropertyName));
    this.OnAsyncCompleted(new AsyncCompletedEventArgs(e.Error, e.Cancelled, null));
}

DoWork 事件在工作线程中引发。它创建一个 Progress 对象并调用基类的最重要的函数 - FindSumOfMultiplesOfSeedsBelowMax()。请注意,这些都包含在 Try/Catch 块中,如果抛出 OperationCanceledException,它会将 DoWorkEventArgs.Cancel 属性设置为 true,当 RunWorkerCompleted 事件引发时,这将传输到 RunWorkerCompletedEventArgs.Canceled 属性。

此外,请仔细阅读此方法的注释。当您在 VS2010 中运行代码并且 FindSumOfMultiplesOfSeedsBelowMax() 抛出 OperationFailedException 时,VS2010 会捕获它并弹出“xxxException 未被用户代码处理”对话框。这是因为我们没有捕获它,在异常到达 BackgroundWorker 之前,VS2010 必须捕获它。

FirstArgIsMultipleOfSecondArg() 中调用 BackgroundWorker.ReportProgress() 时,会引发 ProgressChanged 事件。事件处理程序会引发自己的 ProgressChanged 事件,该事件不同于 BackgroundWorker 引发的事件。SolverViewModel 订阅此事件以更新进度条。BackgroundWorkerProgressChanged 事件在 UI 线程中引发,因此无需将调用封送回 UI 线程。

最后,当 FindSumOfMultiplesOfSeedsBelowMax() 完成执行时(无论是通过成功完成整个计算,还是被用户提前中止,或在计算过程中抛出了某些异常),都会引发 RunWorkerCompleted 事件。无论原因如何,事件处理程序都会引发一个 PropertyChanged 事件来更新 Answer。同样,SolverViewModel 订阅此事件。然后,它引发一个 AsyncCompleted 事件,SolverPresenter 订阅该事件。

杂项编程技巧

Persistence<T> - 该应用程序的一个要求是,当用户关闭和打开应用程序时,我们必须保存和检索设置(MaxSeeds)以及结果(Answer)。Persistence<T> 负责通过公开两个 API - ReadObject()WriteObject(T) 来实现这一点。.NET 3.0 添加了 DataContractSerializer 类来处理通用的持久化场景,我在该应用程序中使用了它。它非常易于使用;只需将 <DataContract> 属性添加到您要持久化的类,并将 <DataMember> 属性添加到您实际要保存/检索的成员。生成的 XML 文件如下所示

<?xml version="1.0" encoding="utf-8"?>
<Solver xmlns:i="http://www.w3.org/2001/XMLSchema-instance" 
         xmlns="http://schemas.datacontract.org/2004/07/DataBinding">
  <Answer i:nil="true" />
  <Max>1000</Max>
  <Seeds xmlns:d2p1="http://schemas.microsoft.com/2003/10/Serialization/Arrays">
    <d2p1:int>3</d2p1:int>
    <d2p1:int>5</d2p1:int>
  </Seeds>
</Solver>

Persistence<T> 负责所有事情,包括捕获相关异常,并用用户友好的错误消息(希望如此)重新抛出 OperationFailedException。同样,不要在此处捕获通用异常。

PositiveIntegerSetCollection - 这个类继承自 .NET 4.0 新增的 HashSet<int>。我们需要一个数学集合而不是一个通用集合,因为我们不想要重复的元素,而这正是集合的作用。它与基类的唯一区别是,如果该类的客户端尝试添加负整数,它会抛出 ArgumentOutOfRangeException,如下所示

public new bool Add(int value)
{
    Contract.Requires<ArgumentOutOfRangeException>(value > 0, 
                        "Must be a positive integer.");
    return base.Add(value);
}

它需要 new 关键字,因为基类的 Add() 不是虚拟的,不能被覆盖。第一条语句是 Code Contracts,我在此检查先决条件。我非常喜欢它的语法,因为它具有可读性。合同要求 value>0;否则,我将抛出带有特定错误消息的 ArgumentOutOfRangeException。当然,**不要**在您的代码中捕获此异常,因为如果它确实发生,那是一个 bug(而不是异常),并且“处理”该 bug 的唯一方法是调试问题,修复它,然后重新分发软件。在您的代码中捕获它对您没有任何好处。让您的应用程序在用户面前悲惨地崩溃;他们会告诉您,您没有彻底测试您的应用程序,而这本应是您的责任,而且一开始就不应该发生。

不幸的是,在撰写本文(2010 年 6 月)时,Code Contracts 尚未完全集成到 VS2010 中,尽管 .NET 4.0 已经支持它。换句话说,即使您从未下载 Code Contracts,您也可以毫无错误地构建此应用程序,但运行它时会遇到运行时错误,因为抛出异常需要 Code Contracts Rewriter。这就是为什么我提供了两个下载版本,一个包含异常抛出,一个不包含。

结论

我展示了 MVP-VM 模式如何帮助您在产品质量的 Windows Forms 应用程序中创建尽可能精简的 form.cs 文件。

参考文献

  • [CLARK] Jason Clark, p. 218, “Framework Design Guidelines” Second Edition, Krzysztof Cwalina, Brad Abrams, 2008
  • [CWALINA/ABRAMS] p.227, “Framework Design Guidelines” Second Edition, Krzysztof Cwalina, Brad Abrams, 2008
  • [KURATA] “Doing Objects in Visual Basic 2005”, Deborah Kurata, 2007

历史

  • 2010/6/17:初始发布。
© . All rights reserved.