c# – How to properly implement the MVP model?

Question:

I am trying to figure out how to improve WinForms experience and separate logic and presentation. Stumbled upon an MVP pattern with short examples and am trying to figure out how to use it. At the moment I have the following code, but it seems to me that I am doing something wrong. In addition, the implementation of multithreading does not fit into this model at all. Hence the question of how to properly implement the MVP design pattern in WinForms?

View implementation

interface ICustomer
{
    string FirstName { get; set; }
    string LastName { get; set; }
    string SurName { get; set; }
    string Address { get; set; }
    string Code { get; set; }

    event Action Search;
    event Action Cancel;
}

public partial class Customer : Form, ICustomer
{
    public string FirstName
    {
        get { return lblFirstName.Text; }
        set { lblFirstName.Text = value; }
    }

    public string LastName
    {
        get { return lblLastName.Text; }
        set { lblLastName.Text = value; }
    }

    public string SurName
    {
        get { return lblSurname.Text; }
        set { lblSurname.Text = value; }
    }

    public string Address
    {
        get { return lblAddress.Text; }
        set { lblAddress.Text = value; }
    }

    public string Code
    {
        get { return txtCode.Text; }
        set { txtCode.Text = value; }
    }

    public event Action Search;
    public event Action Cancel;

    public Customer()
    {
        InitializeComponent();
    }

    private void btnSearch_Click(object sender, EventArgs e)
    {
        Search();
    }

    private void btnCancel_Click(object sender, EventArgs e)
    {
        Cancel();
    }
}

Presenter implementation

class CustomerPresenter 
{
    public ICustomer View;
    public ICustomerModel Model;

    public CustomerPresenter(ICustomer view, ICustomerModel model)
    {
        View = view;
        Model = model;

        View.Search += View_Search;
        View.Cancel += View_Cancel;
    }

    private void View_Search()
    {
        new Task(() => {
            View.FirstName = "Alex Krass";
        }).Start();
    }

    private void View_Cancel()
    {

    }
}

Model and call

My model is still empty, as I understand there should be no problems with it, calling all this disgrace through Unity IoC.

class UnityIoC
{
    private static UnityContainer unityContainer;

    public static UnityContainer Instance 
    {
        get
        {
            if (unityContainer == null) CreateContainer();
            return unityContainer;
        }
    }

    private static void CreateContainer()
    {
        unityContainer = new UnityContainer();
        unityContainer.RegisterType<ICustomer, Customer>();
        unityContainer.RegisterType<ICustomerModel, CustomerModel>();
    }
} 

static void Main()
{
    Application.EnableVisualStyles();
    Application.SetCompatibleTextRenderingDefault(false);

    ApplicationContext context = new ApplicationContext()
    {
        MainForm = UnityIoC.Instance.Resolve<CustomerPresenter>().View as Form
    };
    context.MainForm.Show();

    Application.Run(context);
}

Well, accordingly, when I click on the Search button, I cannot update the UI, do I really have to forward the TextBox itself to ICustomer in the Presenter and call BeginInvoke? Or am I just misunderstanding something about the MVP implementation?

UPD:

The issue of updating the UI without blocking it seems to have been resolved through the use of a timer, when information needs to be TaskScheduler as the Task executed and through the TaskScheduler , when it needs to be updated after the Task executed.

**Вариант 1**

timer = new Timer() { Interval = 1000 };
timer.Tick += timer_Tick;
timer.Start();

private void timer_Tick(object sender, EventArgs e)
{
    UpdateView();
}

**Вариант 2**
task = new Task(new Action(UpdateModel));
task.ContinueWith(new Action<Task>(UpdateView), TaskScheduler.FromCurrentSynchronizationContext());
task.Start();


private void UpdateView(Task task = null)
{
    view.SomeVal = SomeVal;
    view.SomeValNext = SomeValNext;
}

Answer:

Questions on the interpretation and implementation of design patterns are almost always holy, so I immediately warn you that everything that is written below is my personal vision based on my own logic, understanding and practice of using WinForms. You can read about another version of the interpretation and implementation of MVP in the article on Habré

By analogy with biology, let's start with the simplest single-celled (single-window) applications containing only simple controls (buttons, pictures, text fields with or without input).

1. Unicellular

Let's decide on the components of the pattern and what elements of the application belong to them.

  • M – Model – a separate class encapsulating work with data. All operations on data are performed only in it. If we look at the model under a microscope, then in it we can consider multi-threaded or asynchronous processing of heavy calculations, calls to services and databases, and other layers inherent in models.

  • V – View – a visual representation of the model data. This includes all the controls of our, so far the only one, forms that actually display our model from the selected angle.

  • P – Presenter – I will not invent a special translation, we will focus on a long but more or less precise definition – a component that is responsible for receiving data from a model and knows how and when it needs to be displayed, and also processes user input, pulls the model for the appropriate methods and handles model events.

In the case of the simplest, the presenter will be the form class, in the logic of which we will organize the transfer of data from the model to the controls for the presentation of data and the processing of events of the controls to transfer the user actions to the model.

2. Multicellular

When there are more than one windows or complex controls appear, the previous application model still has the right to exist, but it becomes inconvenient to work with it.

Let's add a presenter class, for which our single-celled cells described above will act as a representation. Its main task is to pass the necessary piece of the model to private resenters to work, aggregate events from them and send these events in the desired order and quantity to the model, as well as route model events to private presenters. For private presenters, the general one will act as a model. You can also implement multi-threaded / asynchronous access to the model in it, since it only deals with the model and subordinate presenters, not the UI elements that private presenters are responsible for.

Thus, it turns out that our application consists of many simple, relatively independent fragments, under the guidance of a senior presenter, ensuring their coherence.

3. Changing the pattern

The application grows and becomes more complex and at some point even the multicellular model becomes inconvenient.

It's time to remember that WinForms supports DataBinding. And in this regard, you can slightly change the template. On the net, it is often referred to as MVPVM .

Here a new component appears – VM – view-model, and the role of the presenter changes slightly. VM is essentially a slice of the model for display. The tasks of the presenter will no longer include transferring data to subordinate views, but only creating bindings to the necessary VMs, binding bindings to views and processing events. And we, in principle, can refuse a common presenter, because when creating the next control, you can simply transfer the necessary VM to it, and he will do the rest himself. True, in complex cases, we will still need an aggregator for events, especially if events from different controls are interconnected or conflict with each other.

This model allows the most flexible expansion of functionality and application mass-scale.

4. Conclusion

Despite the fact that I considered each model separately, in fact, they smoothly flow from one another as the application becomes more complex, and in practice, in its pure form, none of the considered models practically occurs and even a seemingly simple application may require complex combined solutions. and vice versa. Just follow the logic, common sense and the principle – "easier is better".

Well, accordingly, when I click on the Search button, I cannot update the UI, do I really have to forward the TextBox itself to ICustomer in the Presenter and call BeginInvoke? Or am I just misunderstanding something about the MVP implementation?

According to the options I proposed, you are using the second option, and the only thing missing is the events in the CustomerPresenter that the search is complete and you can pick up data for display, which the form can subscribe to. Or, define and implement the DataUpdate method in ICustomer and implement the DataUpdate method on the Customer form (or override the Form's Control.Update method) and call it from the CustomerPresenter when the search is complete and the results can be accessed from the main thread.

short algorithm:

  • using a button (or other action) on the Customer form, send a search request to the CustomerPresenter .
  • CustomerPresenter passes request to model
  • the model activates the search in a separate thread. The components of the main thread are from now on free for other useful work.
  • The check is complete, the results are loaded into the model. The model fires an event that the search is complete.
  • CustomerPresenter receives a search end event, gets the search results from the model, and:
    1. calls the UpdateData method (approximate name) of the form and passes the search results in parameters.
    2. activates the end event of a long running operation
  • Customer , gets the search results from the CustomerPresenter , exactly how it depends on the previous item, and displays them.

There is no idle operation of the interface during the search.

It is also possible according to the existing scenario:

  • using a button (or other action) on the Customer form, send a search request to the CustomerPresenter .
  • CustomerPresenter calls the lookup method on the model on a separate thread. The components of the main thread are from now on free for other useful work.
  • Once finished in the thread search using Invoke:
    1. we call the UpdateData method (approximate name) of the form and pass the search results in parameters.
    2. we activate the event of the end of a long-running operation
  • Customer , gets the search results from the CustomerPresenter , exactly how it depends on the previous item, and displays them.

You can also wrap a long search in an asynchronous method, and inside wait for the completion of the search flow without hanging the interface, but I can only imagine this option in theory, I did not do it with my hands.

Scroll to Top