Archive for the ‘ASP.NET’ Category.

When the Overlords Demand WebForms You Can Still Do MVC

I have worked for several clients that seem reluctant to use ASP.NET MVC for whatever reason. I’m sure you can imagine all of them and come up with counter-arguments…I know I have. If I am forced to use WebForms I have a simple MVC pattern that I like to use that allows me to write testable code (always my main concern). The following code should be considered a start point. I have used it on two projects, but I have had to make additions to suit each. I am only showing you enough to demonstrate how everything is hooked together.

Note: IContainer (not listed) represents an IoC container. In my case, I am implementing it with Unity.

The View

public interface IView
{
    IList<string> Errors { get; set; }
    void RedirectToRoute(string route, object routeParameters);
}

public abstract class PageBase<T> : Page where T : ControllerBase
{
    protected T Controller { get; private set; }
    protected IContainer Container { get; private set; }

    protected PageBase()
    {

    }

    public void RedirectToRoute(string route, object routeParameters)
    {
        Response.RedirectToRoute(route, routeParameters);
    }

    protected abstract bool NeedToAuthenticate();
    protected abstract void Authenticate();

    protected override void OnLoad(EventArgs e)
    {
        if (NeedToAuthenticate())
        {
            Authenticate();
            return;
        }

        Container = Application.GetContainer();
        Controller = Container.Resolve<T>();

        if (Controller == null)
        {
            throw new Exception("Could not resolve controller for " + typeof(T));
        }

        Controller.PageData = (Page.RouteData != null) ? Page.RouteData.Values : new RouteValueDictionary();

        Controller.SetView(this);

        if (IsPostBack)
        {
            Controller.PostBack();
        }
        else
        {
            Controller.Load();
        }

        Controller.SubscribeToEvents();

        base.OnLoad(e);

    }

}

The Controller

public abstract class ControllerBase<TView> : ControllerBase where TView : IView
{
    public TView View { get; set; }

    public override void SetView(object view)
    {
        View = (TView)view;
    }
}

public abstract class ControllerBase : IPageController
{
    public IDictionary<string, object> PageData { get; set; }

    protected ControllerBase()
    {
        PageData = new Dictionary<string, object>();
    }

    public object this[string key]
    {
        get
        {
            if (PageData != null && PageData.ContainsKey(key))
            {
                return PageData[key];
            }

            return null;
        }
    }

    /// <summary>
    /// Called during initial request.
    /// </summary>
    public virtual void Load()
    {
        // See PageBase
    }

    /// <summary>
    /// Called during a postback
    /// </summary>
    public virtual void PostBack()
    {
        // See PageBase
    }

    public virtual void SetView(object view)
    {
        // See PageBase
    }

    public virtual void SubscribeToEvents()
    {
        // See PageBase
    }
}

The Model

Not shown. The model will be a data type that corresponds to the UI. Typically, the View interface will contain a Model property that allows the Controller to get/set this information.

Hooking it up

What makes it happen is the following line of code (in PageBase.cs):

Controller = Container.Resolve<T>();

This allows the Controller to be created via IoC which allows you to inject dependencies through the constructor.

This is basically all there is to it, but I think an example might be in order. So, say I need a page that allows an admin to view system errors, I will create the following 4 files:

  • SystemErrorsView.aspx (WebForm)
  • ISystemErrorsView.cs (View interface)
  • SystemErrorsController.cs (the Controller)
  • SystemErrorsModel.cs (optional, may not be needed)
public interface ISystemErrorsView : IView
{
    void SetSystemErrors(IList<string> systemErrors);
}

public partial class SystemErrorsView : PageBase<SystemErrorsController>, ISystemErrorsView
{
    public void SetSystemErrors(IList<string> systemErrors)
    {
        // display the errors somehow
    }
}

public class SystemErrorsController : ControllerBase<ISystemErrorsView>
{
    public SystemErrorsController(/* inject stuff here using IoC */)
    {
        // injected members
    }

    public override void Load()
    {
        // use stuff that you injected to get data for the view
        var systemErrors = ...
        View.SetSystemErrors(systemErrors);
    }

    public override void SubscribeToEvents()
    {
        // see below
    }

}

Events

As you can see from above, Controller-to-View communication occurs directly through the ISystemErrorsView interface. View-to-Controller communication happens using events.

Let’s continue the example. We will add a Clear button to the View that allows an admin to clear all system errors.

The first thing we do is add an event to the View interface:

public interface ISystemErrorsView : IView
{
    event EventHandler WhenCleared;
    void SetSystemErrors(IList<string> systemErrors);
}

Now, we need to implement this in the code-behind for the View. Assume we have a button on the page called btnClear. We are going to intercept the buttons click event and then raise the WhenCleared event that we defined on the View interface:

public partial class SystemErrorsView : PageBase<SystemErrorsController>, ISystemErrorsView
{
    public event EventHandler WhenCleared;

    private void btnClear_Clicked(object sender, EventArgs e)
    {
        if (WhenCleared != null)
        {
            WhenCleared(sender, e);
        }
    }
}

Lastly, we need to subscribe to the event in our Controller:

public class SystemErrorsController : ControllerBase<ISystemErrorsView>
{
    public override void SubscribeToEvents()
    {
        View.WhenCleared += WhenCleared;
    }

    public void WhenCleared(object sender, EventArgs e)
    {
         // update database
         // refresh view
    }

}

Conclusion

I don’t like writing conclusions. That’s all I have. All typical warnings and caveats apply.