Windows Form development is still happening, is fast, and is reasonably easy to understand how to get started with. It's event-driven and hasn't changed much since Visual Basic. You drag controls onto a canvas, double-click the controls to open their event methods, and write what's supposed to happen.

Thus, Frankenstein's monster was born.

I'm taming the monster with a pattern I'm calling the ViewService. I won't create a Gang of Four class diagram, but basically a ViewService is a way of separating the code that manages ViewModels, and is a useful approach in Windows Forms. It's directly analagous to domain services and models.

How often have you written or maintained code that looks like this? (note this is pseudocoding, not actual code.)

class MainForm
{
    void buttonLoadData(object sender, EventArgs e)
    {
        var cn = new Connection(_connString);
        var cmd = new Command(cn, "select a.*, b.* from Customer a join Order b on a.CustomerId = b.CustomerId");
        var reader = cmd.ReadResults();
        until (reader.Eof)
        {
            var co = ConvertReaderToCustomerOrder(reader.Read());
            grid1.Row.Add(new Row(co.Name, co.Zip);
            AddOrdersToGridRow(grid1.Rows[grid1.Rows.Length-1], co)
            if (co.Type = 1) { grid1.Rows[grid1.Rows.Length-1].BackgroundColor = Blue;}
            else if (co.Type = 3) { grid1.Rows[grid1.Rows.Length-1].BackgroundColor = Orange;}
        }
        
    }
    
    void AddOrdersToGridRows(Row row, CustomerOrder order, Connection cn)
    {
        foreach (var orderitem in order.Orders)
        {
            var cmd = new Command();
            cmd.Connection = cn;
            cmd.CommandText = String.Format ("select * from lineitems where OrderId = %1",  orderitem.OrderId);
            var items = ToLineItems(cmd.ExecuteQuery());
            row["Items"] = AddItemsToGrid(items, row);
            if (order.Type = 3 and items.Count() > 15) {row.BackgroundColor = Red;}
        }
        CheckIfMoreOrdersHaveArrivedAndPrintThem();
        
    }
    
    void buttonRefreshData(object sender, EventArgs e)
    {
        buttonLoadData(sender, e);
    }
    
}

The problems with the above code could occupy us for awhile, and they add up to one word: complication.

  • Events are doing too many things
  • Events are called directly
  • Tight coupling
  • Inconsistent naming and coding style

Here's how I recommend clearing up this kind of code by applying the ViewService pattern.

  1. Group form events together
  2. Group together methods that only apply to this form
  3. Group methods that could apply to a replacement form together, potentially into a service
  4. Form events present data, or call a method to preserve data

Let's apply these steps to the above.

The code below is pretty sparse and incomplete. The goal is to give you the idea of what to do, not provide a full-fledged implementation.

Group form events and methods

This is organizational, and clarifies what the user is doing vs what the developer is doing. (Methods collapsed for clarity.)


+ void AddOrdersToGridRows(Row row, CustomerOrder order, Connection cn)...

#region "Control Events"

+ void buttonLoadData(object sender, EventArgs e)...
+ void buttonRefreshData(object sender, EventArgs e)...

#endregion

Better methods and names, and events call custom methods instead of being treated as custom methods

class MainForm
{
    List<CustomerOrderView> _customerOrders = new List<CustomerOrderView>();
    
    + void GetData()...
    + void LoadControls()
    
    void buttonLoadData(object sender, EventArgs e)
    {
        GetData();
        LoadControls();
    }
    void buttonCancel(object sender, EventArgs e)
    {
        LoadControls();
    }
}

Already, we're gaining clarity.

Separate the data calls into a ViewService

Imagine the ViewService is going to be resued in a web application. That means it doesn't accept or process form controls, and is UI agnostic.

This is the same Dependency Injection pattern used in web applications. The difference is that CustomerOrderViewService isn't a domain service, it's specific to this view of the data.


class MainForm 
{
    ICustomerOrderViewService _customerOrderViewService = null;
    List<CustomerOrderView> _customerOrders = new List<CustomerOrderView>();

    
    + void MainForm(ICustomerOrderViewService)...
    
    void GetData()
    {
        _customerOrders = _customerOrderViewService.Get(txtCustomerId.Text);
    }
    
    void LoadControls()
    {
        if (_customerOrders == null) { GetData(); }
        gridOrders.DataSource = _customerOrders;
    }

}

//These two classes would be in separate files, and *could* be in a separate namespace
//to emphasize the decoupling.

class CustomerOrderViewService: ICustomerOrderViewService
{
    ICustomerOrderService _customerOrderService = null;
    
    + public CustomerOrderViewService(ICustomerOrderService _customerOrderService)...
    

    public List<CustomerOrderView> Get(string customerId)
    {
        //This is the call to the *domain service*. It might call the database directly, or might in turn call a web api.
        
        //Returns type CustomerOrder
        var customerOrders = _customerOrderService.GetOrdersByCustomer(string customerId);
        
        //Mapping
        return customerOrders.Select(a => a.ToCustomerOrderView());
    }
    
}

class static CustomerOrderViewServiceHelpers
{
    public static ToCustomerOrderView (this CustomerOrder customerOrder)
    {
        return new CustomerOrderView()
        {
            Name = customerOrder.Name,
            CustomerType = customerOrder.CustomerType,
            etc....
        }
    }
}

The Happy Wrap Up

By applying the ViewService pattern, we can separate Windows Form code into cleaner areas of concern, making our code clear, testable, maintainable and replaceable.