Saturday, April 19, 2008

Use MVP on Windows Forms

In my previous post, I suggested a MVP pattern for ASP.Net controls. This post intend to implement MVP for .Net Windows Forms. Note that since Windows Forms data are in memory objects, it is much easier to do two-way databinding.

(1) Model ---Reference Data
Data Entry windows almost always need reference data to guide user input. A dropdown list filled with 50 US States is a perfect example. Naturally, we would want a simple type structure class and a complex type storage class to hold 50 states for databinding purposes:

public struct State
{
public State(int? id, string name)
{
_StateID = id;
_StateName = name;
}

private int? _StateID;
public int? StateID
{
get { return _StateID;}
set { _StateID = value; }
}

private string _StateName;
public string StateName
{
get { return _StateName; }
set { _StateName = value; }
}
}

public class States
{
private static List _StateList = new List();
static States()
{
_StateList.Add(new State(0, "MA"));
....

_StateList.Add(new State(null, "(empty)")); // add (empty) for user to de-select to
}
public static List StateList
{
get { return _StateList; }
}
}

(2) Model --- Entity Data
Suppose UI is designed to present User an address for View/Edit/Create, we then need an entity to represent Address:

public class Address
{
private int? _StateID;
public int? StateID
{
get { return _StateID; }
set { _StateID = value; }
}
}

Note Database normalization requires only saving StateID to database and avoid duplicating StateName.


(3) Presenter --- one way databinding to View
Upon View calling its single entry point, a Presenter will initialize View, Load Data from Model and Execute Business logic (e.g. show, update, add):

public interface IAddressView
{
void ShowReferenceData();
void ShowCurrentAddress();
}

class AddressPresenter
{
private IAddressView _view;
public AddressPresenter(IAddressView view)
{
_view = view;
}
public void Render(int? UserID)
{
LoadEntityData(UserID);
_view.ShowReferenceData();
_view.ShowCurrentAddress();
}

private ASC.ReferenceData.State _State;
public ASC.ReferenceData.State State
{
get { return _State;}
set { _State=value;}
}

private ASC.Entity.Address _Address;
private void LoadEntityData(int? UserID)
{
// simulate retrival of Address from Database
_Address = new ASC.Entity.Address();
if (UserID >0) _Address.StateID = 3;
// All UserID <=0 will have empty selection of state in its Address Entity;

foreach (ASC.ReferenceData.State s in ASC.ReferenceData.States.StateList)
{
if (s.StateID == _Address.StateID)
{
_State = new ASC.ReferenceData.State(_Address.StateID, s.StateName);
break;
}
}

}
public void Update()
{
// Save Entity to Database
}
public void Add()
{
// Insert Entity to Database
}
}
Note that two properties in Address Entity Data are represented as a single property of type ASC.ReferenceData.State. This will enable loading into a ComboBox for ease of databinding.


(4) View --- Implement IView and Reverse DataBind
As before, View need to implement IView with consideration of reference data
public partial class AddressChangeForm : Form, IAddressView
{
public void ShowReferenceData()
{
this.cbState.DataSource = ASC.ReferenceData.States.StateList;
this.cbState.DisplayMember = "StateName";
}

public void ShowCurrentAddress()
{
cbState.DataBindings.Add("SelectedItem", _Presenter, "State",false,DataSourceUpdateMode.OnPropertyChanged, new ASC.ReferenceData.State(null,"(empty)"));
}
Note that Reference data are push to View using one-way databinding, while Presenter data are push to View with change probagated backwards through OnPropertyChanged.
Presenter entry point will be called upon user action:
private void btnGetCurrent_Click(object sender, EventArgs e)
{
cbState.DataBindings.Clear();
_Presenter = new AddressPresenter(this);
_Presenter.Render(1078);
}

And a new presenter need to be created upon User Add New action:
private void btnAdd_Click(object sender, EventArgs e)
{
.....
cbState.DataBindings.Clear();
_Presenter = new AddressPresenter(this);
_Presenter.Render(-1);
......
_Presenter.Add();
cbState.DataBindings.Clear();
.....
}

No comments: