Saturday, February 6, 2010

Exploring the MVP Pattern

Windows Forms are still number one choice when it comes to development for Windows platform. Windows Forms are easy and proven. In most cases you use things out-of-the-box. If you need something special, you have the ability to make custom controls work the way you want. For most enterprise applications I would dare to call Windows Forms near to perfect. So what is this post about then? Imagine that you’re building a typical enterprise application with layered architecture. You’ve organized your layers perfectly, looking like this:

Perfect layered architecture

Next imagine that you’re done with developing your data and business layers. You’re happy since you have a nice, clean and loosely coupled design so far. Everything seems perfect as it can be. The next step is to work on your presentation layer. You are all excided! Of course, you choose to use Windows Forms for that. Shortly, after starting the development of presentation layer, you start to feel weird. Your feeling of nice, clean codebase starts to disappear. You start to see your BLL classes mixed in various event handlers. A lot of (object sender, EventArgs e) stuff makes you feel sad. You see code like txtName.Text and listViewOrders.Items all over the place. Little by little, it gets harder to gain control of the complexity.

Reason behind this post

First off, if you already have experience in using MVP pattern in Windows Forms, then it may be possible that you will not benefit that much from reading this post.

There are a lot of sources about implementation of the MVP pattern in Windows Forms applications. However, while exploring the pattern on my own, I didn’t find a lot of applications with full source code provided. Also, example applications were very simple, far from what can be seen in real-world applications. This made my understanding of MVP pattern harder. In that sense, I have set following goals for this post:

  • Build an working application by using MVP pattern.
  • Provide full source for it.
  • The application needs to be more complex than a simple “Hello World” example
  • Explain benefits of using MVP in Windows Forms applications 

The problem

My application consists of a single form. I’ll call the form Money Transfer Form. The form is used to initiate money transfer from one account to another. Each account has an ID and the name of the account owner. An account can be either of type Personal or Company. The actual transfer can be either Standard, International, Extended (no idea what that means). When transferring money, one needs to provide amount which needs to be of specific currency. Here are the other requirements/rules:

  • When transfer type is Standard the amount that can be transferred is limited to 500
  • When transfer type is International the amount cannot be lower then 200, and the currency needs to be in dollars. However, if dollar exchange rate at the moment of transfer is below 1.3, then the minimum amount that’s allowed needs to be 700. User of application needs to be notified when this happens. Also, in such case, another currency can be used for transfer.
  • When transfer type is Extended and both accounts are Company, an additional description of the transfer needs to be specified. Normally, users cannot enter any kind of description unless these conditions are not satisfied. The description cannot exceed 20 characters. If it does, the user will be asked a if he wants to reset the description (clear it). Also, as long as description is larger then 20 characters, this will be indicated with different color in UI. 
  • When user enters account number, the name of the account owner and account type has to be displayed on the form.
  • Before actual money transfer is initiated, application needs to ask the user to confirm such action. Also, when transfer has been done, the user needs to be informed.

Note that these rules really don’t make any sense, but can provide us with the complexity that often is found in real-world applications.

Domain design

Here is my domain model:

Domain Model 

The core domain model is very simple. To most interesting part is MoneyTransfer class. 

Validation

Since I have a lot of business rules, I have to think about validation. There has been a lot of discussions about validation in domain model. There are generally 2 different approaches

1. Let the entity validate itself by using some kind of IsValid() method.
2. Use external services to perform validation on entities.

The first method is fine, but lacks some flexibility. The main argument against it is that the same entity can be valid in one context and not valid in some other context. Personally, I prefer to keep my validation away from entities. Validation services can also be reused. This way, in one context a one set of rules can be used, while in another context a different set of rules can be used. There are different approaches to implementing the validation services, for example introducing IValidator<T> interface, but I will not use that approach just to keep the things simpler. Here is my rule interface:

    public class ValidationResult
    {
        public string Message { get; set; }
        public bool IsValid { get; set; }
    }
    public interface IMoneyTransferRule
    {
        ValidationResult Validate(MoneyTransfer moneyTransfer);
    }

The interface is very simple since we only have Validate method on it. Every rule must implement this interface. I’ll show some rules implementing IMoneyTransferRule interface:

“When transfer type is Standard the amount that can be transferred is limited to 500”

public class StandardLimitRule : IMoneyTransferRule
{
    #region IMoneyTransferRule Members

    public ValidationResult Validate(MoneyTransfer moneyTransfer)
    {
        if (moneyTransfer.Type == TransferType.Standard)
        {
            if (moneyTransfer.Amount > 500)
                return new ValidationResult()
                           {
                               IsValid = false,
                               Message = "When transfer type is 'Standard', amount is limited to 500."
                           };
        }
        return new ValidationResult {IsValid = true};
    }

    #endregion
}

“When transfer type is Extended and both accounts are Company, an additional description of the transfer needs to be specified. Normally, users cannot enter any kind of description unless these conditions are not satisfied. The description cannot exceed 20 characters.”

public class ExtendedTransferRule : IMoneyTransferRule
{
    #region IMoneyTransferRule Members

    public ValidationResult Validate(MoneyTransfer moneyTransfer)
    {
        if (moneyTransfer.Type == TransferType.Extended)
        {
            if (moneyTransfer.From.Type == AccountType.Company && moneyTransfer.To.Type == AccountType.Company)
            {
                if (string.IsNullOrEmpty(moneyTransfer.Description) || moneyTransfer.Description.Length > 20)
                {
                    return new ValidationResult
                               {
                                   IsValid = false,
                                   Message = "Extended Company-To-Company transfers need to have description. The description needs to be below 20 characters."
                               };
                }
            }
        }
        return new ValidationResult { IsValid = true };
    }

    #endregion
}

This was my validation strategy. Now, I’ll go to repositories.

Repositories

There are two repositories in my application: ICurrencyRepository and IAccountRepository. I’ll provide two in-memory implementations of both.

AccountRepository

 public class AccountRepository : IAccountRepository
    {
        public List<Account> _accounts = new List<Account>(); 
        public AccountRepository()
        {
            _accounts.Add(new Account { ID = "1", Name = "Account 1", Type = AccountType.Personal  });
            _accounts.Add(new Account { ID = "2", Name = "Account 2", Type = AccountType.Company });
            _accounts.Add(new Account { ID = "3", Name = "Account 3", Type = AccountType.Personal });
            _accounts.Add(new Account { ID = "4", Name = "Account 4", Type = AccountType.Company });
            _accounts.Add(new Account { ID = "5", Name = "Account 5", Type = AccountType.Personal });
            _accounts.Add(new Account { ID = "6", Name = "Account 6", Type = AccountType.Company  });
            _accounts.Add(new Account { ID = "7", Name = "Account 7", Type = AccountType.Company  });
            _accounts.Add(new Account { ID = "8", Name = "Account 8", Type = AccountType.Company  });
            _accounts.Add(new Account { ID = "9", Name = "Account 9", Type = AccountType.Personal });
            _accounts.Add(new Account { ID = "10", Name = "Account 10", Type = AccountType.Personal });
        }
        #region IAccountRepository Members

        public Account Load(string ID)
        {
            return _accounts.Where(m => m.ID == ID).FirstOrDefault();
        }
        public void Withdraw(string ID, double amount)
        {
            // we don't actually do anything here
        }
        public void Deposit (string id, double amount)
        {
            // we don't actually do anything here
        }
        #endregion
    }

CurrencyRepository

  public class CurrencyRepository : ICurrencyRepository
    {
        private List<Currency> _currencies = new List<Currency>(); 
        public CurrencyRepository()
        {
            _currencies.Add(new Currency { ID = "1", Name = "USD", Country = "US", ExchangeRate = 1.6 });
            _currencies.Add(new Currency { ID = "2", Name = "GBP", Country = "United Kingdom", ExchangeRate = 1.0 });
            _currencies.Add(new Currency { ID = "3", Name = "EUR", Country = "EU", ExchangeRate = 1.95 });
        }

        public Currency Load(string id)
        {
            return _currencies.Where(c => c.ID == id).FirstOrDefault(); 
        }

        public List<Currency> GetAll()
        {
            return _currencies.ToList(); 
        }
    }

Another thing worth mentioning is TransferFailedException class:

public class TransferFailedException : Exception 
{
    public List<ValidationResult> FailedValidations { get; set; } 
    public TransferFailedException(ValidationResult[] failedValidations)
    {
        this.FailedValidations = new List<ValidationResult>(); 
        this.FailedValidations.AddRange(failedValidations); 
    }
}

MVP Pattern

MVP stands for Model-View-Presenter. The relationships between M and V and P are:

Model-View-Presenter

Controller is mediating between Model and View. Unlike in MVC here we don’t have direct connection between the Model and the View. The Presenter is responsible for updating both the Model and the View. This variant of MVP is called Passive View, since View acting is kind of dumb. The whole process goes like this:

  1. Events are received on View, the View passes them to the Presenter.
  2. Presenter decides how and if the Model needs to be updated.
  3. Presenter initiates any needed changes on the View.

In this approach View is very simplified and worries only about the very presentation. There are no decisions being made in the View. The Presenter actually controls the logic flow. This is how Passive View variant of MVP works. There are also two other variants of MVP: Presentation Model and Supervising Controller. I will not talk about them now. This picture of Passive View may be more expressive:

Model-View-Presenter

That was theory. Here’s the practice:

Here it is how my form looks like:

Example MVP Application

First, I’ll abstract my frmMoneyTransfer with an interface:

public interface IMoneyTransferView
{
    string Account1ID { get; set; }
    string Account1Name { get; set; }
    string Account1Type { get; set; }
    string Account2ID { get; set; }
    string Account2Name { get; set; }
    string Account2Type { get; set; }
    string Description { get; set; }
    Currency Currency { get; }
    double Amount { get; set; }
    TransferType Type { get; set; }

    bool AskIfSure();
    void ShowMessage(string message);
    void DisplayDescriptionField();
    void HideDescriptionField();
    void PopulateCurrencyList(List<Currency> currencyList);
    bool AskQuestion(string message);
    void ExpandCurrencyBox();
    void SetDescriptionDifferentColor();
    void SetDescriptionNormalColor();
}

The frmMoneyTransfer implements IMoneyTransferView interface.

public partial class frmMoneyTransfer : Form , IMoneyTransferView
{
    ...
}

I’ll also need my MoneyTransferPresenter class

public class MoneyTransferPresenter
{
    ...
}

Connecting View and Presenter

The connection between View and Presenter is simple one, they hold references to each other. Here is the Presenter constructor:

public class MoneyTransferPresenter
{
    ...
    private IMoneyTransferView View { get; set; }
    public MoneyTransferPresenter(IMoneyTransferView view)
    {
        ...
        this.View = view;
        ...

    }
}

The View implementation also holds reference to Presenter

public partial class frmMoneyTransferFrom : Form , IMoneyTransferView
{    ...    
    MoneyTransferPresenter Presenter { get; set; }     
    public frmMoneyTransferFrom()    
    {        
        InitializeComponent();        
        this.Presenter = new MoneyTransferPresenter(this);     
    }    
    ...
}

Connection between Model and Presenter

This one is even simpler. Presenter holds reference to Model. The Model does not even need to know about that.

public class MoneyTransferPresenter
{
    ...
    private MoneyTransfer MoneyTransfer { get; set; }

    public MoneyTransferPresenter(IMoneyTransferView view)
    {
        ...
        this.MoneyTransfer = new MoneyTransfer();
        ...
    }
}

I have said earlier that:

“Events are received on View, the View passes them to Presenter.”

Here’s how it looks in practice when user presses Process Transfer button.

private void btnProcessTransfer_Click(object sender, EventArgs e)
{
    Presenter.ProcessTransfer();
}

The view only called Presenter.ProcessTransfer() method. Here’s another example:

private void txtAmount_TextChanged(object sender, EventArgs e)
{
    Presenter.AmountChanged(); 
}

I also said this:

Presenter decides how and if the Model needs to be updated.”

public class MoneyTransferPresenter
{
    ...
    public void AmountChanged()
    {
        this.MoneyTransfer.Amount = View.Amount;
    }
    ...
}

Then again, I’ve said this as well:

Presenter initiates any needed changes on the View

Below, you can see how Presenter tells the View what to do and updates it appropriately:

public void DescriptionChanged()
{
    if (View.Description.Length > 20)
    {
        bool answer =
            View.AskQuestion(
                "Warning : Your description is larger then 20 characters. Your transfer may fail. Do you want to clear it?");
        if (answer == true)
        {
            View.Description = string.Empty;
        }
        else
        {
            View.SetDescriptionDifferentColor();
        }
    }
    else
    {
        View.SetDescriptionNormalColor();
    }
    MoneyTransfer.Description = View.Description;
}

What is the benefit of using MVP?

  • We’ve managed to control the complexity by separating concerns.
  • Code in frmMoneyTransfer form is simplified and easy to understand.
  • Code in Presenter is clean and simple because it accesses the View using well defined interface – IMoneyTransferView
  • We have better structure of the code which improves understandability, maintainability, readability.
  • We have got a more testable code.

Note on source code provided

I did not use any IoC container just to keep the code simpler and more focused on MVP pattern. That’s why I have things like new MoneyTransferPresenter() and new AccountRepository() all over the place.

I also strongly suggest you to go through the source code and examine the various parts of the application.

Conclusion

My conclusion is that, in absolutely all, but the most simplest situations, should MVP pattern be used when building presentation layers in Windows Forms. The pattern is easy to learn and the benefit of using it is great.

3 comments:

  1. Thank you very much for this nice article with example. It addresses the gap for which you have written it. But I found the link for the source code is broken. Can you please arrange for it? It will be useful.

    ReplyDelete
  2. Great article, thank you. Can you please provide a new link for the source code? Thank you!

    ReplyDelete
  3. Guys, unfortunately I don't have the source code for the article, anymore. I might code it again and let you know.

    ReplyDelete