Geeks With Blogs
Mike Nichols - SonOfNun Technology If I were the captain on a big steamboat...

Implementing the Model-View-Presenter has been fun. I appreciate it's tendency to enforce discipline in keeping any business decisions out of the View layer. I was always bewildered when I would read that the business logic should be separated from the 'code-behind' source and then see millions of DataSet examples where that was tons of business logic embedded in Web Pages.

However, trying to separate the presentation logic away and keep the client as dumb as possible presents (forgive the pun) some new challenges. Keeping the presenter interacting with only the interfaces of both the IView and IFacade (or IService or whatever...IModel) is straightforward so long as basic property setters and getters on the view are the requirements. But when you get into nested collections within collections AND want to keep the client dumb, then it is time to reach into the patterns hat for a solution.

Specifically, I wasn't sure how to have the Presenter authoritative for logic decisions while still keeping the DataBind() calls in place on my DataSource controls (like a Repeater). My first reaction is to create a kind of 'placeholder' property for the DataItem and have that iteratively set during binding for processing in the Presenter layer. For example, perhaps a different list in a dropdown list needs to appear BASED ON WHAT THE DATA ITEM IS. I want my presenter to inject the correct list, but how to do that during binding?

A refactoring has emerged that I'll call Replace Placeholder Property With Visitor. The Visitor pattern is well-suited to exactly this kind of databinding dilemma. No doubt, some will think it's heavyhanded, but I have found it elegant and keeps responsibilities between the view and presenter where they belong.

To start, say we have Person we want to edit that has a collection of Addresses. We want to bind a Repeater control on a collection of Labels so that for each label, we have a blank Address form. Within each address form, we have a drop down list of StreetTypes (Rd, St, Ln, etc). So it looks like this.

To use the Visitor pattern to inject the list of StreetTypes into the DropDown list, we'll abstract away the contracts and have the actors only deal with those. First, the ISaveAddressViewItem interface This will live in the Presentation project so the Presenter will interact with any implementation of it. Before creating that let's have it inherit from a more generic interface (explain later):

    public interface IViewRepeaterItem

    {

    }

Ok, now let's create the contract that has the property names we expect.

    public interface ISaveAddressViewItem:IViewRepeaterItem

    {

        string Label { get;}

        string SuiteApartment { get;set;}

        string PostalCode { get;set;}

        string StreetField1Value { get;set;}

        string StreetField1StreetType { get;set;}

        string StreetField2Value { get;set;}

        string StreetField3Value { get;set;}

 

        IList<StreetTypes> StreetTypes { set;}

 

 

    }

Next, let's create a custom RepeaterItem that will implement this interface. We'll put this in its own project where we create our other custom controls (NOT App_Code!):

    public class SaveAddressViewRepeaterItem : RepeaterItem, ISaveAddressViewItem,IViewRepeaterItem

    {

        public SaveAddressViewRepeaterItem(int itemIndex, ListItemType itemType) : base(itemIndex, itemType)

        {

        }

 

        public string Label

        {

            get { return ControlUtil.FindFirstControlById<System.Web.UI.WebControls.Label>(this, "Label").Text; }

        }

 

        public string SuiteApartment

        {

            get { return ControlUtil.FindFirstControlById<TextBox>(this, "SuiteApartment").Text; }

            set { ControlUtil.FindFirstControlById<TextBox>(this, "SuiteApartment").Text = value; }

        }

 

        public string PostalCode

        {

            get { return ControlUtil.FindFirstControlById<TextBox>(this, "PostalCode").Text; }

            set { ControlUtil.FindFirstControlById<TextBox>(this, "PostalCode").Text = value; }

        }

 

        public string StreetField1Value

        {

            get {return ControlUtil.FindFirstControlById<TextBox>(this,"StreetField1Value").Text; }

            set {ControlUtil.FindFirstControlById<TextBox>(this,"StreetField1Value").Text = value; }

        }

 

        public string StreetField1StreetType

        {

            get { return ControlUtil.FindFirstControlById<DropDownList>(this, "StreetField1StreetTypes").SelectedValue; }

            set {

                ControlUtil.FindFirstControlById<DropDownList>(this, "StreetField1StreetTypes").SelectedValue = value;}

        }

 

        public string StreetField2Value

        {

            get { return ControlUtil.FindFirstControlById<TextBox>(this, "StreetField2Value").Text; }

            set { ControlUtil.FindFirstControlById<TextBox>(this, "StreetField2Value").Text = value; }

        }

 

        public string StreetField3Value

        {

            get { return ControlUtil.FindFirstControlById<TextBox>(this, "StreetField3Value").Text; }

            set { ControlUtil.FindFirstControlById<TextBox>(this, "StreetField3Value").Text = value; }

        }

 

        public IList<StreetTypes> StreetTypes

        {

            set

            {

                DropDownList list = ControlUtil.FindFirstControlById<DropDownList>(this, "StreetField1StreetTypes");

                if (list != null)

                    list.DataSource = value;

            }

        }

 

 

    }

Before we look at the Repeater that will consume this, let's look at the contract for the Presenter that the ISaveAddressViewItem's parent will interact with during databinding

    public interface IPresenterVisitor

    {

        void PostVisit(IViewRepeaterItem item);

        void SetItemVisit(IViewRepeaterItem item);

        void LoadItemVisit(IViewRepeaterItem item);

    }

Now let's look at the Repeater control (also in the Class project, not in the Web folder) we create to bring the IViewRepeaterItem and IPresenterVisitor together. We need to tell this somewhat generic repeater control what kind of custom RepeaterItem to instantiate in order to get our implementation, so quick and dirty I add a ItemType property that I can declare in the aspx form. We also need to tell the Repeater control who the Presenter is that implements IPresenterVisitor. Finally, we need to override certain events to wireup our conversation between the View and Presenter during binding. This is where the visitor pattern shines.

    public class ViewRepeater : Repeater

    {

        public ViewRepeater()

        {

            ItemDataBound += new RepeaterItemEventHandler(SetItem);

        }

 

        private Type _itemType;

        [Bindable(true)]

        public Type ItemType

        {

            get { return (Type)ViewState["ItemType"]; }

            set

            {

                if(value.GetInterface(typeof(IViewRepeaterItem).ToString())==null)

                {

                    throw new InvalidCastException("The RepeaterItem must implement the IViewRepeaterItem interface");

                }

                ViewState["ItemType"] = value;

            }

        }

 

        private IPresenterVisitor _visitor;

 

        public IPresenterVisitor Visitor

        {

            get { return _visitor; }

            set { _visitor = value; }

        }

 

 

        protected override RepeaterItem CreateItem(int itemIndex, ListItemType itemType)

        {

            IViewRepeaterItem item = Activator.CreateInstance(ItemType, itemIndex, itemType) as IViewRepeaterItem;

            ((RepeaterItem)item).DataBinding += new System.EventHandler(LoadItem);

            return (RepeaterItem)item;

        }

        /// <summary>

        /// Used to assign datasource to controls before binding.

        /// </summary>

        /// <param name="sender"></param>

        /// <param name="e"></param>

        void LoadItem(object sender, System.EventArgs e)

        {

            Visitor.LoadItemVisit((IViewRepeaterItem)sender);

        }

        /// <summary>

        /// Our controls have been bound, but now the presenter can set hte values to the model object's value.

        /// </summary>

        /// <param name="sender"></param>

        /// <param name="e"></param>

        void SetItem(object sender, RepeaterItemEventArgs e)

        {

 

            if (e.Item.ItemType.Equals(ListItemType.Item) ||

                e.Item.ItemType.Equals(ListItemType.AlternatingItem) ||

                e.Item.ItemType.Equals(ListItemType.EditItem))

            {

                Visitor.SetItemVisit((IViewRepeaterItem)e.Item);

            }

        }

        /// <summary>

        /// This is used to iterate the items and allow the presenter to perform actions on each item such as during a Save operation.

        /// </summary>

        public void Post()

        {

            foreach(RepeaterItem item in Items)

            {

                if (item is IViewRepeaterItem)

                {

                    Visitor.PostVisit((IViewRepeaterItem)item);

                }

 

            }

        }

    }

The Presenter can now interact with the View during databinding by implementing the three methods in IPresenterVisitor

        public void PostVisit(IViewRepeaterItem item)

        {

            ISaveAddressViewItem viewItem = (ISaveAddressViewItem) item;

            if (new NonEmptyStringSpecification().IsSatisfiedBy(viewItem.PostalCode) ||

                new NonEmptyStringSpecification().IsSatisfiedBy(viewItem.StreetField1Value))

            {

                ContactAddressDTO dto = GetAddressByLabel(viewItem.Label);

                bool isNull = !new NonNullSpecification<ContactAddressDTO>().IsSatisfiedBy(dto);

                if ((!isNull && dto.StateMarker.Equals(DTOStateMarker.DELETED) ||

                    isNull))

                {

 

                    PostalCodeDTO postalCode = new PostalCodeDTO(0, viewItem.PostalCode);

                    StreetAddressDTO streetAddress = new StreetAddressDTO(0, postalCode, viewItem.SuiteApartment, null, null);

                    StreetTypes type = new ValueObjectFieldBrowser<StreetTypes>().GetBy("Abbreviation", viewItem.StreetField1StreetType);

                    streetAddress.StreetFields.Add(new StreetFieldDTO(0, 0, viewItem.StreetField1Value, type));

                    if (new NonEmptyStringSpecification().IsSatisfiedBy(viewItem.StreetField2Value))

                    {

                        streetAddress.StreetFields.Add(new StreetFieldDTO(0, 1, viewItem.StreetField2Value, StreetTypes._NONE));

                    }

                    if (new NonEmptyStringSpecification().IsSatisfiedBy(viewItem.StreetField3Value))

                    {

                        streetAddress.StreetFields.Add(new StreetFieldDTO(0, 2, viewItem.StreetField3Value, StreetTypes._NONE));

                    }

                    ContactAddressDTO newAddress = new ContactAddressDTO(0, viewItem.Label, streetAddress);

                    Contact.Addresses.Add(newAddress);

                }

            }

 

        }

        public void LoadItemVisit(IViewRepeaterItem item)

        {

            ((ISaveAddressViewItem) item).StreetTypes = StreetTypes.LIST_ALL;

 

        }

        public void SetItemVisit(IViewRepeaterItem item)

        {

            ISaveAddressViewItem viewItem = (ISaveAddressViewItem)item;

            /*//Check if Contact has addresses,

            if so then search for an address matching item.Label then populate the fields as necessary

            if not, then do nothin*/

 

            if (new NonNullSpecification<ContactDTO>().IsSatisfiedBy(Contact))

            {

                if (Contact.Addresses.Count > 0)

                {

                    ContactAddressDTO address = GetAddressByLabel(viewItem.Label);

                    if (address != null)

                    {

                        bool isDeleted = address.StateMarker.Equals(DTOS.DTOStateMarker.DELETED);

                        viewItem.StreetField1Value = isDeleted?"":address.StreetAddress.StreetFields[0].Value;

                        viewItem.StreetField1StreetType = isDeleted ? "" : address.StreetAddress.StreetFields[0].StreetType.Abbreviation;

                        if (address.StreetAddress.StreetFields.Count > 1)

                        {

                            viewItem.StreetField2Value = isDeleted ? "" : address.StreetAddress.StreetFields[1].Value;

                        }

                        if (address.StreetAddress.StreetFields.Count > 2)

                        {

                            viewItem.StreetField3Value = isDeleted ? "" : address.StreetAddress.StreetFields[2].Value;

                        }

                    }

                }

            }

        }

The details of the Presenter in relating to the underlying model and how the View is injected are left out...there's plenty on the web about how to do this. What is intersting here is the LoadItemVisit method...if we had complex logic such as presenting a different StreetTypes list based on some property in the ViewItem (I know, that's stupid), we could do it right here in our presenter... just where it should be. The View remains dumb to logic and is relying on the Presenter to inject collections and values into its controls.

Once you get used to the "dual-dispatch" of the Visitor pattern, it becomes a very useful way of getting rid of some properties that are temporal to hold values while processing collections.

Resources

Posted on Saturday, September 9, 2006 10:30 PM OOP, Patterns, Architecture , C# | Back to top


Comments on this post: Visitor Pattern For DataBinding ListControls with Model View Presenter

# re: Visitor Pattern For DataBinding ListControls with Model View Presenter
Requesting Gravatar...
Nice, I like. Using the Visitor pattern with the View could solve a few problems I've run into in the past...thanks for sharing.
Left by Billy McCafferty on Apr 16, 2007 11:25 AM

# re: Visitor Pattern For DataBinding ListControls with Model View Presenter
Requesting Gravatar...
Thanks, this is exactly what i need!

What exactly is the ControlUtil ? Can you post the source?
Left by Christopher on Jul 18, 2008 11:46 PM

# Drug Watch
Requesting Gravatar...
It's good code
Left by hystoryk on Oct 08, 2008 4:39 AM

Your comment:
 (will show your gravatar)


Copyright © Mike Nichols | Powered by: GeeksWithBlogs.net