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.