Feeds:
Posts
Comments

Archive for the ‘Web UI’ Category

In a recent posting we outlined a novel idea for separating how we want web UI controls to look from where they get their data. As we were converting our application to this framework we became aware of a cut-paste pattern in the coding of our builders, and more importantly their tests, that we wanted to eliminate. We thought more about our design goals and decided that on the developer side we really wanted our system to work like Fluent NHibernate mappings. We wanted to somehow configure the mapping and be able to test the mappings with an infrastructure test instead of hand coded ones for each model. Another new goal was to better enforce mapping controls to the model to which the form data would be bound when posted. Finally, we wanted to give the designer more freedom to choose presentation controls in the UI.

So, with those goals in mind, lets take a look at our current UI and then we’ll delve into the code that makes it work:

    <div>
        <%= Model.TextBoxFor(x=>x.Street)
            .WithLabel("Street:")
            .Width("400px") %>
    </div>
    <div>
	<%= Model.TextBoxFor(x=>x.City)
	    .WithLabel("City:") %>
    </div>
    <div>
	<%= Model.DropDownListFor(x=>x.State)
	    .WithDefault("Select", "")
	    .WithLabel("State:") %>
	</div>
    <div>
	<%= Model.TextBoxFor(x=>x.ZipCode)
	    .WithLabel("Zip:").Width("50px") %>-<%= 
	    Model.TextBoxFor(x=>x.ZipPlus).Width("50px")%>
    </div>

You’ll notice that we’re back to having lambdas instead of hiding them in an InputProvider. We could still have this but it is an extra layer that serves only as a passthrough… and it would get us back to cut-paste testing. The important thing here is the lambdas bind to the Model object, not the Domain, so we know they will match up to the Model we are using to receive the submitted form data.

Now let’s skip to the Map:

public class AddressUIMap : UIMap<Address, AddressModel>
{
	protected AddressUIMap(Address address, IRepository repository)
		: base(address)
	{
		ConfigureFreeText(x => x.Street, x => x.Street);
		ConfigureFreeText(x => x.City, x => x.City);
		ConfigureFreeText(x => x.ZipCode, x => x.ZipCode);
		ConfigureFreeText(x => x.ZipPlus, x => x.ZipPlus);

		ConfigureChoiceList(x => x.State, x => x.State,
		                    state => state.Name, 
		                    state => state.StateCode)
			.WithItems(repository.GetAll<State>);
	}
}

Here you’ll note that we’re not specifying TextBox or DropDownList. This is where the freedom for the designer comes in to play. We want to specify the data binding but not the final control type, just its general type. So a FreeText could become a Hidden, TextBox, TextArea, or Password. A ChoiceList could become a DropDownList or RadioButtonList. And so on. The final choice is up to the designer, within certain bounds. As a bonus, because this is context free, we could easily update the architecture to support binding the output to some other format instead of HTML controls if we wanted to.

The other thing you may notice is the ChoiceList .WithItems() method takes a Func. This allows us to late bind, fetching the data only if you ask for a control that uses this mapping. As a result we don’t have to put mock expectations all over our tests just because we constructed a map.

But wait, it gets better. We automatically map all the properties that are named the same between the Domain and Model object. This means you only have to specify the special cases, minimizing the work you have to do in the map:

public class AddressUIMap : UIMap<Address, AddressModel>
{
	protected AddressUIMap(Address address, IRepository repository)
		: base(address)
	{
		ConfigureChoiceList(x => x.State, x => x.State,
		                    state => state.Name, 
		                    state => state.StateCode)
			.WithItems(repository.GetAll<State>);
	}
}

Next we have a sample of the methods that are being called from the page:

public DropDownListData DropDownListFor(Expression<Func<TModel, object>> source)
{
	var uiMap = TryGetRequestedMap(source);
	var listUiMap = uiMap.TryCastTo<IChoiceListMap>();
	return listUiMap.AsDropDownList().WithIdPrefix(_idPrefix);
}

public TextBoxData TextBoxFor(Expression<Func<TModel, object>> source)
{
	var uiMap = TryGetRequestedMap(source);
	var freeTextUiMap = uiMap.TryCastTo<IFreeTextMap>();
	return freeTextUiMap.AsTextBox().WithIdPrefix(_idPrefix);
}

public HiddenData HiddenFor(Expression<Func<TModel, object>> source)
{
	var uiMap = TryGetRequestedMap(source);
	var freeTextUiMap = uiMap.TryCastTo<IFreeTextMap>();
	return freeTextUiMap.AsHidden().WithIdPrefix(_idPrefix);
}

I included the definition of HiddenFor so you could see that we can convert the mapping data to the requested type from the same starting data just by calling AsHidden() instead of AsTextBox(). Note that we are still relying on FluentWebControls to build the actual HTML controls.

Another exciting new capability we have added is automatic table generation. We define a ListUIMap for the class we’d like to present in a table:

public class AddressListUIMap : ListUIMap<Address,AddressModel>
{
	public AddressListUIMap(IEnumerable<Address> items)
		: base(items)
	{
		ConfigureColumn(x => x.Street, x => x.Street);
		ConfigureColumn(x => x.City, x => x.City);
		ConfigureColumn(x => x.ZipCode, x => x.ZipCode);
		ConfigureColumn(x => x.ZipPlus, x => x.ZipPlus);
		ConfigureColumn(x => x.State, x => x.State.ToNonNull().Name);
	}
}

As with the UIMap, in ListUIMap we auto-map all the same-named columns so you only have to configure the special cases:

public class AddressListUIMap : ListUIMap<Address,AddressModel>
{
	public AddressListUIMap(IEnumerable<Address> items)
		: base(items)
	{
		ConfigureColumn(x => x.State, x => x.State.ToNonNull().Name);
	}
}

This gives the designer the ability to display one, some, or all of the columns in any order:

<%= Model.AsTable()
       .WithId("addresses")
       .WithColumn(Model.DataColumnFor(x=>x.State).WithHeader("State"))
       .WithColumn(Model.DataColumnFor(x=>x.City).WithHeader("City"))
%>

We are continuing to build out this framework and have already converted all our InputProvider based interfaces to this improved architecture.

co-authored with Shashank Shetty

Read Full Post »