Thursday, November 19, 2009

Robust ASP.NET navigation - Remove hardroce in query string.

Url's in application are fastly spreading across code, having different parameters and formats. This causes several problems during development and support:
  • Urls mistyping
  • Parameters mistyping
  • No easy way to search for usage of particular page url on parameter in code.
  • Hard to introduce any kind of url rewriting scheme.
  • Violates DRY principle
In this article I'll describe solution that allow:
  • Compile time checking of url and parameter mistyping
  • Parameters type cheking
  • Easy searching of url and parameter usage
The general idea - is to add for each page a "*Url" class that will contain page url and all possible arguments for the page.
Lets introduce url class for page "/Module/Example.aspx" with one mandotary parameter CustomerID (not nullable):

public ExampleUrl : IUrl
{
[UrlParameter("CID")]
public long CustomerID { get; set; }
public string GetPageLocation()
{
return "~/Module/Example.aspx";
}
}

From this point, all links to Example.aspx - should be composed using ExampleUrl instance, like this one:
(new ExampleUrl() {.CustomerID = 3}).MakeNavigableUrlForMe();
This will allow to search links by page or request parameters using R# - search for usages feature and to check all types of mistyping in compile time.

But to make all this stuff to work, some sort of Request assembler/disassembler is required. Lets name it UrlBuilder - a simple class whose responsibilities is to generate request from strongly typed url and populate url by parameters from request.
public class UrlBuilder
{
public string GetUrl(IUrl PageUrl);
public void FillInUrl(IUrl PageUrl, NameValueCollection ValueCollection);
}
That class can be easily developed using TDD manner, here are my tests:
[TestFixture]
public class UrlBuilderTest
{
#region Public Methods

[Test]
public void EmptyUrlRendersToUnresolvedPageUrl()
{
var Builder = new UrlBuilder();
var PageUrl = new EmptyUrl();

var HttpUrl = Builder.GetUrl(PageUrl);

Assert.That(HttpUrl, Is.EqualTo("~/Module/Example.aspx"));
}

[Test]
public void UrlWithAttributedPropertyRendersToPageUrlWithGetParameter()
{
var Builder = new UrlBuilder();
var PageUrl = new ExampleUrl();

var HttpUrl = Builder.GetUrl(PageUrl);

Assert.That(HttpUrl, Is.EqualTo("~/Module/Example.aspx?CID=0"));
}

[Test]
public void UrlWithValuesRendersToPageUrlWithGetParameter()
{
var Builder = new UrlBuilder();
var PageUrl = new AttributedUrl()
{
CustomerID = 123,
};

var HttpUrl = Builder.GetUrl(PageUrl);

Assert.That(HttpUrl, Is.EqualTo("~/Module/Example.aspx?CID=123"));
}

[Test]
public void NameValueCollectionMappedToMarkedAttributeInUrl()
{
var Builder = new UrlBuilder();
var PageUrl = new AttributedUrl();
var GetValues = new NameValueCollection();

GetValues.Add("CID", "825");

Builder.FillInUrl(PageUrl, GetValues);

Assert.That(PageUrl.CustomerID, Is.EqualTo(825));
}
// Tests for many parameters, and different types are skipped.
}

Ok, Now IUrl is not responsible for "Making Navigable Url For Me", it's left for UrlBuilder.

And the last thing left - integrate PageUrl and UrlBuilder to WCSF. As you now, WCSF using MVP pattern. Presenter is a good integration point for both PageUrl and UrlBuilder. Let's parametrize BasePresenter generic class with IUrl and force BaseView to initialize parameters form request.
public class BasePresenterWithUrl<TView, TUrl> : BasePresenter<TView>
where TView : class
where TUrl : class, IUrl, new()
{
public TUrl Url { get; set; }

public override void InitUrlFromRequest(NameValueCollection Parameters)
{
Url = new TUrl();
UrlBuilder.FillInUrl(Url, Parameters);
}
}

public class BasePage<TPresenter, TView> : Page
where TPresenter : BasePresenter<TView>
where TView : class
{
protected override void OnInit(EventArgs Args)
{
Presenter.InitUrlFromRequest(Request.Params);
base.OnInit(Args);
}
}

With this implementation, page presenter contains property Url, that is filled with values from request and ready to operate during regular presenter events like OnViewLoaded and OnViewInitialized.

Ok, ready to rumble, here is an example usage of "url pattern":
// Url setting for "Add item" link
public override void OnViewInitialized()
{
View.SetUrlForAdd(UrlBuilder.GetUrl(new ExampleUrl(){ ClientID = 0 }));
}
// Example page presenter
public override void OnViewLoaded()
{
if(Url.ClientID == 0)
{
View.SetMode(Mode.Add);
} else {
View.SetMode(Mode.Edit);
View.SetCustomer(_CustomersGateway.ById(Url.ClientID);
}
}

No comments:

Post a Comment