如何在Windows窗体应用程序中使用C#.NET正确实现MVC模式

8
我一直在网上寻找.NET中MVC框架设置的示例实现。我找到了许多示例,但它们似乎在某些方面存在不同。我有一本关于设计模式的书,其中描述了MVC起源于Smalltalk,所以我阅读了几篇关于该语言实现的讨论。接下来是我编写的一个示例项目,利用我收集到的正确实现,但我对其中的一些细节感到困惑。
我遇到的一个问题是对象构建的正确顺序。这是我Program.cs中的实现。
Model mdl = new Model();
Controller ctrl = new Controller(mdl);
Application.Run(new Form1(ctrl, mdl));

视图: 我有几个问题不确定。首先,如果视图只是从模型中读取数据进行更新,但包含对其的引用,那么有什么阻止我从视图调用控制器对模型的调用?程序员应该忽略他们暴露给模型成员函数的事实吗?另一个想法是,通知视图模型已更新的事件是否会发送某种状态对象以供视图进行更新。

public interface IView
{
    double TopSpeed { get; }
    double ZeroTo60 { get; }

    int VehicleID { get; }
    string VehicleName { get; }
}

/// <summary>
/// Assume the form has the following controls
/// A button with a click event OnSaveClicked
/// A combobox with a selected index changed event OnSelectedIndexChanged
/// A textbox that displays the vehicles top speed named mTextTopSpeed
/// A textbox that displays the vehicles zero to 60 time named mTextZeroTo60
/// </summary>

public partial class Form1 : Form, IView
{
    private IController mController;
    private IModel mModel;

    public Form1(IController controller, IModel model)
    {
        InitializeComponent();

        mController = controller;
        mController.SetListener(this);
        mModel = model;
        mModel.ModelChanged += new ModelUpdated(mModel_ModelChanged);
    }

    void mModel_ModelChanged(object sender, EventArgs e)
    {
        mTextTopSpeed.Text = mModel.TopSpeed.ToString();
        mTextZeroTo60.Text = mModel.ZeroTo60.ToString();
    }

    public double TopSpeed { get { return Double.Parse(mTextTopSpeed.Text); } }

    public double ZeroTo60 { get { return Double.Parse(mTextZeroTo60.Text); } }

    public int VehicleID { get { return (int)mComboVehicles.SelectedValue; } }

    public string VehicleName { get { return mComboVehicles.SelectedText; } }

    #region Form Events

    private void OnFormLoad(object sender, EventArgs e)
    {
        mComboVehicles.ValueMember = "Key";
        mComboVehicles.DisplayMember = "Value";
        mComboVehicles.DataSource = new BindingSource(mModel.VehicleList, null);
    }

    private void OnSelectedIndexChanged(object sender, EventArgs e)
    {
        mController.OnSelectedVehicleChanged();
    }

    private void OnSaveClicked(object sender, EventArgs e)
    {
        mController.OnUpdateVehicle();
    }

    #endregion
}

控制器: 我对我实现的控制器的唯一真正问题是,似乎可以在没有明确分配视图的情况下构建控制器。我可以完全忽略视图,但这意味着我将向控制器的函数传递参数来更新模型,这似乎完全错过了重点。
public interface IController
{
    void OnUpdateVehicle();
    void OnSelectedVehicleChanged();
    void SetListener(IView view);
}

class Controller : IController
{
    private IModel mModel;
    private IView mView = null;

    public Controller(IModel model)
    {
        mModel = model;
    }

    public void OnUpdateVehicle()
    {
        if(mView == null)
            return;

        mModel.UpdateVehicle(mView.VehicleID, mView.TopSpeed, mView.ZeroTo60);
    }

    public void SetListener(IView view)
    {
        mView = view;
    }

    public void OnSelectedVehicleChanged()
    {
        if (mView == null)
            return;
        mModel.SelectVehicle(mView.VehicleID);
    }
}

模型: 在我的表单中,我有一个下拉框,其中列出了我的伪数据库中提供的车辆列表。因此,我觉得我的表单实际上应该实现多个视图/模型。一个视图专门用于列出可能的车辆,并带有相应的控制器/模型,另一个视图用于显示所选车辆的信息,具有自己的控制器/模型。

public delegate void ModelUpdated(object sender, EventArgs e);

public interface IModel
{
    event ModelUpdated ModelChanged;

    void UpdateVehicle(int id, double topSpeed, double zeroTo60);
    void SelectVehicle(int id);

    double TopSpeed { get; }
    double ZeroTo60 { get; }
    IDictionary<int, string> VehicleList { get; }
}

// class for the sake of a pseudo database object
class Vehicle
{
    public int ID { get; set; }
    public string Name { get; set; }
    public double TopSpeed { get; set; }
    public double ZeroTo60 { get; set; }

    public Vehicle(int id, string name, double topSpeed, double zeroTo60)
    {
        ID = id;
        Name = name;
        TopSpeed = topSpeed;
        ZeroTo60 = zeroTo60;
    }
}


class Model : IModel
{
    private List<Vehicle> mVehicles = new List<Vehicle>()
    {
        new Vehicle(1, "Civic", 120.0, 5.0),
        new Vehicle(2, "Batmobile", 9000.0, 1.0),
        new Vehicle(3, "Tricycle", 5.0, 0.0)
    };

    private Vehicle mCurrentVehicle;

    public Model()
    {
        mCurrentVehicle = mVehicles[0];
    }

    public event ModelUpdated ModelChanged;

    public void OnModelChanged()
    {
        if (ModelChanged != null)
        {
            ModelChanged(this, new EventArgs());
        }
    }

    public double TopSpeed { get { return mCurrentVehicle.TopSpeed; } }

    public double ZeroTo60 { get { return mCurrentVehicle.ZeroTo60; } }

    public IDictionary<int, string> VehicleList
    {
        get 
        {
            Dictionary<int, string> vDict = new Dictionary<int, string>();
            foreach (Vehicle v in mVehicles)
            {
                vDict.Add(v.ID, v.Name);
            }

            return vDict as IDictionary<int, string>;
        }
    }

    #region Pseudo Database Calls

    public void SelectVehicle(int id)
    {
        foreach (Vehicle v in mVehicles)
        {
            if (v.ID == id)
            {
                mCurrentVehicle = v;
                OnModelChanged(); // send notification to registered views
                break;
            }
        }
    }

    public void UpdateVehicle(int id, double topSpeed, double zeroTo60)
    {
        foreach (Vehicle v in mVehicles)
        {
            if (v.ID == id)
            {
                mCurrentVehicle.TopSpeed = topSpeed;
                mCurrentVehicle.ZeroTo60 = zeroTo60;
                OnModelChanged(); // send notification to registered views
                break;
            }
        }
    }

    #endregion
}

在这篇文章的结尾,我想说的是,我需要一些指导,告诉我我所做的是否代表了真正的MVC实现,并希望有人能够解决上述问题。非常感谢您的任何建议。

我不是MVC概念的专家,但我认为你遇到视图能够调用模型中函数的问题的原因是因为你没有将模型从数据库中抽象出来。你的模型应该是纯对象,而其他东西(数据库上下文)应该处理保存更改。然后你将纯模型对象传递给视图,它们上面没有可调用的方法。 - Jess
1
最初,我实现了MVP模式,其中我的模型只是表示数据库表对应列的对象。然后,我有一个与Presenter绑定的服务,用于执行数据库交互并将其分配给该模型。但问题在于,我仍然有一种方法可以为该模型分配数据,而不仅仅是构造函数调用。如果我希望我的模型可更新,它必须能够以某种方式接受赋值。除非当我从视图保存数据到表中时,我构造一个新对象传递给数据库更新函数。 - geek_factorial
我不认为这真的是异常的。只要你无法将更改提交回数据库,那么在模型上设置属性有什么问题呢? - Jess
你说得对,我认为我可能会更喜欢对我的初始MVP设置进行变化。在这条路线上,在我的视图(View)中,将通过一个表示我的表格的对象模型来更新视图(View),该模型将具有用于绑定视图(View)的事件。然后,Presenter或者View将可以访问视图上的赋值,但是只有Presenter将包含将模型分配到数据库表格的逻辑。 - geek_factorial
1个回答

6
我们需要了解你想做什么。您目前拥有监督控制器的实现。如果您希望从视图中删除模型(以及任何数据绑定),则可以实现被动视图模式。请参见this文章获取更多区别。 Passive View and Supervising Controller
(来源:microsoft.com) 还有马丁·福勒(Martin Fowler)是GUI架构之王(GUI Architectures)。

首先,感谢您的快速回复。我一直在查看Martin网站上的那个页面,但我发现他的一些图表很难理解。此外,在采用这种设置之前,我实际上一直在使用MVP的变体,因为我开始阅读的更多内容都是要求使用观察者模式从模型更新视图,在c#的情况下我使用了事件。您有任何意见,是否应该将我的示例中的组合框作为mvc的单独实例? - geek_factorial

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接