The next page...
Since the previous post was getting a bit lengthy, I decided to split it up into multiple parts. We already covered the groundrules for the architecture, showed how our presenter could get the values from the view, and navigate to another view. All of this in the business layer, clean and perfectly testable (possibly even before the actual aspx-page is written). But let's go into detail on this "navigation to another view", to see how it works.
As shown in the previous post, the code for our continue_click handler was :
This NavigationComponent is an external dependency, defined in the BasePresenter, so all presenters can access it. This dependency is resolved out of our dependency container (Unity), using the code :
In INavigationComponent, we define the contract for the Navigation. Most basically, we need a NavigateTo() method, that takes a PageIdentifier (strong typing is your friend) and optionally the QueryParameters, either as an array of strings, or a NameValueCollection (which is handy to pass the current querystring without altering it). Remember to use the QueryParameter enum names to name the queryparameters, this will allow for strongly typed retrieval of these parameters. Also it will allow easy refactoring and the right-click-find-usages method of resharper to see all the places where a particular parameter is used !
The implementation of INavigationComponent lives obviously in the Web project, since it will need access to the Response.Redirect() method. Also, in this component we will map the PageIdentifiers to physical aspx pages. So we have one central place where all physical paths are located (great for refactoring and maintainability !). In our example it looks like :
The mechanism for ControlIdentifier is analog. The advantages of this approach are tremendous. We can no longer have "dead links" ;simply verify the existence of the physical path for each identifier in a unit test. Also, we can detect "unused pages" : do find usages on a page identifier, and if it's only used in the "PageIdentifier " implementation of the IPageView, then this page is only accessible via the direct URL (e.g. no other page in the web project is linking to it)
[note : as we will see in a later post, this mechanism can also easily be extended to provide functionality to navigate to a page, and return back to the calling page.]
So, the next page. In our example, we have now a first page which let the user type in a username, select a language from a dropdown, and click continue :
As we saw just above, the View_ContinueClicked() handler in the presenter, sets the type username in the SessionComponent, and passes the selected language (by it's localeId) via the querystring to our TestPage2.
Now the functional requirements of our second page is that initially nothing is shown, but we have buttons to toggle the username and language on/off. To spice it up a little, it has to be done via Ajax, and with the use of UserControls (you might never know another page also has to display the username). Also, we need to be able to return to the first page.
Again, we start by asking ourselves the question: what data needs to be displayed, and what events will be triggered. Well, the page obviously will have 3 buttons, and 2 controls to display. So the interface for ITestPage2 will kind of look like :
1: public interface ITestPage2 : IPageView
2: {
3: event EventHandler BackButtonClicked;
4: event EventHandler ToggleUserButtonClicked;
5: event EventHandler ToggleLanguageButtonClicked;
6: bool UserVisible { get; set; }
7: bool LanguageVisible { get; set; }
8: }
Since the user and language will be displayed in controls, it's not the page's responsibility anymore to provide the data [note: for such simple controls, one could argue that the data could be injected in the control from within the page-presenter. But for demonstrative purposes we have chosen to let the controls have more responsibility - and have their own control-presenters]
We will also have 2 simple controls, each with their own view-interface :
1: public interface ITestControl1 : IControlView
2: {
3: Language Language { set; }
4: }
5: public interface ITestControl2 : IControlView
6: {
7: string UserName { set; }
8: }
Note that these interfaces inherit from IControlView and not IPageView. Indeed, controls should have a property ControlIdentifier instead of PageIdentifier to uniquely identify them.
Since these 2 controls are so simple let's just implement their presenters. They both have the same OnViewLoad handler, and then each a specific LoadViewFromModel method :
1: protected override void OnViewLoad(object sender, EventArgs e)
2: {
3: if (!IsPostBack)
4: LoadViewFromModel();
5: }
6: //for the control that displays user
7: protected override void LoadViewFromModel()
8: {
9: View.UserName = SessionComponent.User;
10: }
11: //for the control that displays language
12: protected override void LoadViewFromModel()
13: {
14: int selectedLocaleId = GetQueryParameterAsInt(QueryStringParameter.Language);
15: if (selectedLocaleId==0)
16: selectedLocaleId = 1033; //if no language is supplied, get English as default
17: Language selectedLanguage = ReferenceDataComponent.GetLanguageByLocaleID(selectedLocaleId);
18: View.Language = selectedLanguage;
19: }
As we can see, the control responsible for displaying the user gets it from the session, the control that displays the language get's it from the querystring (with some validation code added). Note that the GetQueryParameter methods take the strongly typed enum as argument, so byebye typing mistakes in queryparameter-names !
So, now we have both our controls that display our data, let's do the presenter for the page. Using the help of Resharper, our presenter is completed in a matter of minutes.
Step1 : type the class name, its baseclass and IView : TestPage2Presenter : BasePresenter<ITestPage2>, and let resharper generate the constructor and empty method implementations
Step 2 : Attach the events from the view to eventhandlers :
1: protected override void SubscribeViewToEvents()
2: {
3: View.BackButtonClicked += new EventHandler(View_BackButtonClicked);
4: View.ToggleLanguageButtonClicked += new EventHandler(View_ToggleLanguageButtonClicked);
5: View.ToggleUserButtonClicked += new EventHandler(View_ToggleUserButtonClicked);
6: }
Step 3 : Intellisense (using tab) created the empty eventhandlers, implement them
1: void View_ToggleUserButtonClicked(object sender, EventArgs e)
2: {
3: View.UserVisible = !View.UserVisible;
4: }
5:
6: void View_ToggleLanguageButtonClicked(object sender, EventArgs e)
7: {
8: View.LanguageVisible = !View.LanguageVisible;
9: }
10:
11: void View_BackButtonClicked(object sender, EventArgs e)
12: {
13: NavigationComponent.NavigateTo(PageIdentifier.TestPage1);
14: }
Step 4 : Implement the OnViewLoad. Remember, function requirements were that initially, both user & language were not displayed :
1: protected override void OnViewLoad(object sender, EventArgs e)
2: {
3: if (!IsPostBack)
4: {
5: View.UserVisible = false;
6: View.LanguageVisible = false;
7: }
8: else
9: {
10: View.UserVisible = View.UserVisible;
11: View.LanguageVisible = View.LanguageVisible;
12: }
13: }
Step 5 : errr...already finished !
So this was actually childishly simple, yielding very readable code, and again, nicely testable, so the requirements can be verified in unit tests.
Implementing the ASPX
Now we can start implementing the actual pages. Since both controls are really simple I will not cover their ascx here, although there are some interesting notes on their baseclass, the BaseUserControl. But first we'll focus on the TestPage2 aspx file.
The markup looks like :
Note how MS Ajax is supported as-is, nothing new to learn here. Also note that we have 2 placeholder controls for dynamically loading our usercontrols, and that the markup has no reference to the usercontrols that will be used. So no physical paths in markup anymore, hello refactoring ! As you surely figured out by now, the physical paths to the usercontrols will be centrally located in our NavigationComponent :
For the codebehind, let Resharper once again do the groundwork, creating method bodies for CreatePresenter, CreateEventHandlers, PageIdentifier, the Events, and the xxxVisible properties. Once resharper is finished, we get :
Again, without typing a single letter besides 'BasePage,ITestPage2'. So now we create a presenter, create the event handlers (with the sweet lambda code, so we don't have any "OnClick" in our markup), specify the PageIdentifier, and implement the UserVisible and LanguageVisible properties. This is where it gets interesting. Remember that our UserVisible property on the view was to indicate that the username control should be shown/not shown. We could have passed this parameter via the querystring, but that would have just cluttered the QueryParameter enum with entries that make no sense outside of this single page. Besides that, we would have to redirect instead of postback to the page to make this work. So the parameter must be kept within the scope of the page ...but how do we do that ... let's find out :
So this property will get/set a parameter on the page, and display the usercontrol (using it's ControlIdentifier ofcourse) when the value is "true". The setting and getting of the parameter is delegated to a bunch of Set/GetPageParameter method, that actually store the parameters in the ViewState. "The ViewState ??" I hear you say ... yes, but all access to the ViewState is tunneled through this methods, so when requirements state that the ViewState cannot be used, a different impementation of these Get/SetPageParameter methods may work with something else. ..but that's food for another post :)
A last thing to note is handling postbacks in a dynamically loaded user control. It would not be correct to just use the buit-in "IsPostBack" property to conclude that a dynamically loaded usercontrol was posted back. What if the page is posting back due to a "toggle visible" , and previously the control was not shown ? Afther the postback, the control will be shown and report "IsPostBack == true" (but in fact it's not posted back). The solution was to keep a page-parameter to indicate that a control was loaded previously. This code is in BaseUserControl and is used in an override of the IsPostBack property, so all presenters can just use IsPostBack.
So, this wraps it up. Most features of the architecture are shown, so it should be clear by now that it's not a framework. It's a way of working, that will yield maintainable, understandable, strongly typed, refactor-friendly, testable code. In next post I will give some examples how to completely ditch the asp.net Forms Authentication and replace it with a few lines of business code, how to plug in a high performant translation engine with a few lines of code, how to get rid of the ViewState, ...
All code used in this example is attached to this post. Have fun !
MvpWebTemplate.zip (558,85 kb)