Welcome to EMC Consulting Blogs Sign in | Join | Help

Remco Blok's Blog

Cascading events over multiple web part connections

This is my first post on this blog. My colleagues told me I found something worth blogging about. So here goes:

I wanted to make cascading events over multiple web part connections work. I’ll use the typical ‘Hello World’ example of cascading DropDownLists. But this time each DropDownList is in its own WebPart. For example I have three DropDownLists, one to select a car make, one to select a model for the selected make and one to select a colour for the selected model, each in its own WebPart. We can use a WebPart connection to allow the model WebPart to consume the selected make from the make WebPart and a WebPart connection to allow the colour WebPart to consume the selected model from the model WebPart. So, you could start with something like this:

public interface IMakeProvider
{
    int MakeId { get; }
}
public interface IModelProvider
{
    int ModelId { get; }
}
public class MakeWebPart : WebPart, IMakeProvider
{
    public int MakeId
    {
        get { return Convert.ToInt32(this.dropDownList.SelectedValue); }
    }

    [ConnectionProvider("Selected Make", "MakeConnection")]
    public IMakeProvider ProvideMakeConnection()
    {
        return this;
    }

    protected override void OnLoad(EventArgs e)
    {
        base.OnLoad(e);

        if (!this.Page.IsPostBack)
        {
            this.dropDownList.DataSource = this.GetMakes();
            this.dropDownList.DataBind();                                
        }
    }
}
public class ModelWebPart : WebPart, IModelProvider
{
    public int ModelId
    {
        get { return Convert.ToInt32(this.dropDownList.SelectedValue); }
    }

    [ConnectionConsumer("Selected Make", "MakeConnection")]
    public void ConsumeMakeConnection(IMakeProvider provider)
    {
        this.dropDownList.DataSource = this.GetModelsByMakeId(this.provider.MakeId);
        this.dropDownList.DataBind();
    }

    [ConnectionProvider("Selected Model", "ModelConnection")]
    public IModelProvider ProvideModelConnection()
    {
        return this;
    }
}
public class ColorWebPart : WebPart
{
    [ConnectionConsumer("Selected Model", "ModelConnection")]
    public void ConsumeModelConnection(IModelProvider provider)
    {
        this.dropDownList.DataSource = this.GetColorsByModelId(this.provider.ModelId);
        this.dropDownList.DataBind();
    }
}

On first load of the page I want to select the first item in each DropDownList by default. The colour WebPart should not read the selected model from the model WebPart until the model WebPart has read the selected make from the make WebPart. However, I suspect the order in which connections between WebParts are established by the WebPartManager depends on the order in which the WebParts are laid out on the page, over which WebParts have no influence and should have no knowledge of. So it could happen that the colour WebPart has its connection to the model WebPart established before the model WebPart has its connection to the make WebPart established. How can the colour WebPart know when it is ok to read the selected model from the model WebPart? Somehow after the connection is established the provider needs to notify the consumer when it is ok for the consumer to read data from the provider.

On PostBacks we only want to DataBind a consumer DropDownList again if the SelectedIndex of the provider DropDownList has changed. So the provider needs to notify the consumer of this event. However, in the WebPart lifecycle PostBack events of web controls are raised before WebPart connections are established. So although the provider WebPart could raise a public event when it handles the SelectedIndexChanged PostBack event of the DropDownList, consumers do not yet have connections established to the provider to subscribe to this event. A solution is to not raise this event until all WebParts on the page have established their connections. The WebPartManager raises the ConnectionsActivated event when all WebParts on the page have established their connections. So we could raise this public event when handling the WebPartManager.ConnectionsActivated event in the WebPart.

See below for a final solution. I added AJAX functionality using UpdatePanels. Note that you cannot add an AsyncPostBackTrigger to the consumer UpdatePanel for the provider DropDownList. Triggers will have to be programmed manually by invoking the UpdatePanel.Update method.

namespace WebParts
{
    using System;

    public interface IMakeProvider
    {
        event EventHandler MakeChanged;

        int MakeId { get; }
    }
}
namespace WebParts
{
    using System;

    public interface IModelProvider
    {
        event EventHandler ModelChanged;

        int ModelId { get; }
    }
}
namespace WebParts
{
    using System;

    public interface IColorProvider
    {
        event EventHandler ColorChanged;

        int ColorId { get; }
    }
}
namespace WebParts
{
    using System;
    using System.Web.UI;
    using System.Web.UI.WebControls;
    using System.Web.UI.WebControls.WebParts;

    public class MakeWebPart : WebPart, IMakeProvider
    {
        private DropDownList dropDownList;

        private bool selectedIndexChanged;

        public event EventHandler MakeChanged;

        public int MakeId
        {
            get { return Convert.ToInt32(this.dropDownList.SelectedValue); }
        }

        [ConnectionProvider("Selected Make", "MakeConnection", AllowsMultipleConnections = true)]
        public IMakeProvider ProvideMakeConnection()
        {
            return this;
        }

        protected virtual void OnMakeChanged(EventArgs e)
        {
            EventHandler eventHandler = this.MakeChanged;
            if (eventHandler != null)
            {
                eventHandler(this, e);
            }
        }

        protected override void OnInit(EventArgs e)
        {
            base.OnInit(e);

            this.WebPartManager.ConnectionsActivated += this.WebPartManager_ConnectionsActivated;
        }

        protected override void CreateChildControls()
        {
            base.CreateChildControls();

            this.dropDownList = new DropDownList
            {
                ID = "MakeDropDownList",
                AutoPostBack = true,
                DataTextField = "Name",
                DataValueField = "Id"
            };
            this.dropDownList.SelectedIndexChanged += this.DropDownList_SelectedIndexChanged;
            this.Controls.Add(this.dropDownList);

            ScriptManager scriptManager = ScriptManager.GetCurrent(this.Page);
            if (scriptManager != null)
            {
                scriptManager.RegisterAsyncPostBackControl(this.dropDownList);
            }
        }

        private void DropDownList_SelectedIndexChanged(object sender, EventArgs e)
        {
            this.selectedIndexChanged = true;
        }

        private void WebPartManager_ConnectionsActivated(object sender, EventArgs e)
        {
            if (!this.Page.IsPostBack)
            {
                this.dropDownList.DataSource = this.GetMakes();
                this.dropDownList.DataBind();
                this.dropDownList.SelectedIndex = 0;

                this.selectedIndexChanged = true;
            }

            if (this.selectedIndexChanged)
            {
                this.OnMakeChanged(EventArgs.Empty);
            }
        }
    }
}
namespace WebParts
{
    using System;
    using System.Web.UI;
    using System.Web.UI.WebControls;
    using System.Web.UI.WebControls.WebParts;

    public class ModelWebPart : WebPart, IModelProvider
    {
        private UpdatePanel updatePanel;

        private DropDownList dropDownList;

        private bool selectedIndexChanged;

        public event EventHandler ModelChanged;

        public int ModelId
        {
            get { return Convert.ToInt32(this.dropDownList.SelectedValue); }
        }

        [ConnectionConsumer("Selected Make", "MakeConnection")]
        public void ConsumeMakeConnection(IMakeProvider provider)
        {
            provider.MakeChanged += this.MakeProvider_MakeChanged;
        }

        [ConnectionProvider("Selected Model", "ModelConnection", AllowsMultipleConnections = true)]
        public IModelProvider ProvideModelConnection()
        {
            return this;
        }

        protected virtual void OnModelChanged(EventArgs e)
        {
            EventHandler eventHandler = this.ModelChanged;
            if (eventHandler != null)
            {
                eventHandler(this, e);
            }
        }

        protected override void OnInit(EventArgs e)
        {
            base.OnInit(e);

            this.WebPartManager.ConnectionsActivated += this.WebPartManager_ConnectionsActivated;
        }

        protected override void CreateChildControls()
        {
            base.CreateChildControls();
            
            this.dropDownList = new DropDownList
            {
                ID = "ModelDropDownList",
                AutoPostBack = true,
                DataTextField = "Name",
                DataValueField = "Id"
            };
            this.dropDownList.SelectedIndexChanged += this.DropDownList_SelectedIndexChanged;
            this.updatePanel = new UpdatePanel
            {
                ID = "ModelUpdatePanel",
                ChildrenAsTriggers = false,
                UpdateMode = UpdatePanelUpdateMode.Conditional                
            };
            this.updatePanel.ContentTemplateContainer.Controls.Add(this.dropDownList);
            this.Controls.Add(this.updatePanel);

            ScriptManager scriptManager = ScriptManager.GetCurrent(this.Page);
            if (scriptManager != null)
            {
                scriptManager.RegisterAsyncPostBackControl(this.dropDownList);
            }
        }

        private void DropDownList_SelectedIndexChanged(object sender, EventArgs e)
        {
            this.selectedIndexChanged = true;
        }

        private void WebPartManager_ConnectionsActivated(object sender, EventArgs e)
        {
            if (this.selectedIndexChanged)
            {
                this.OnModelChanged(EventArgs.Empty);
            }
        }

        private void MakeProvider_MakeChanged(object sender, EventArgs e)
        {
            IMakeProvider provider = (IMakeProvider)sender;
            this.dropDownList.DataSource = this.GetModelsByMakeId(provider.MakeId);
            this.dropDownList.DataBind();
            this.dropDownList.SelectedIndex = 0;

            this.OnModelChanged(EventArgs.Empty);

            this.updatePanel.Update();
        }
    }
}
namespace WebParts
{
    using System;
    using System.Web.UI;
    using System.Web.UI.WebControls;
    using System.Web.UI.WebControls.WebParts;

    public class ColorWebPart : WebPart, IColorProvider
    {
        private UpdatePanel updatePanel;

        private DropDownList dropDownList;

        private bool selectedIndexChanged;

        public event EventHandler ColorChanged;

        public int ColorId
        {
            get { return Convert.ToInt32(this.dropDownList.SelectedValue); }
        }

        [ConnectionConsumer("Selected Model", "ModelConnection")]
        public void ConsumeModelConnection(IModelProvider provider)
        {
            provider.ModelChanged += this.ModelProvider_ModelChanged;
        }

        [ConnectionProvider("Selected Color", "ColorConnection", AllowsMultipleConnections = true)]
        public IColorProvider ProvideColorConnection()
        {
            return this;
        }

        protected virtual void OnColorChanged(EventArgs e)
        {
            EventHandler eventHandler = this.ColorChanged;
            if (eventHandler != null)
            {
                eventHandler(this, e);
            }
        }

        protected override void OnInit(EventArgs e)
        {
            base.OnInit(e);

            this.WebPartManager.ConnectionsActivated += this.WebPartManager_ConnectionsActivated;
        }

        protected override void CreateChildControls()
        {
            base.CreateChildControls();

            this.dropDownList = new DropDownList
            {
                ID = "ColorDropDownList",
                AutoPostBack = true,
                DataTextField = "Name",
                DataValueField = "Id"
            };
            this.dropDownList.SelectedIndexChanged += this.DropDownList_SelectedIndexChanged;
            this.updatePanel = new UpdatePanel
           {
               ID = "ColorUpdatePanel",
               ChildrenAsTriggers = false,
               UpdateMode = UpdatePanelUpdateMode.Conditional
           };
            this.updatePanel.ContentTemplateContainer.Controls.Add(this.dropDownList);
            this.Controls.Add(this.updatePanel);

            ScriptManager scriptManager = ScriptManager.GetCurrent(this.Page);
            if (scriptManager != null)
            {
                scriptManager.RegisterAsyncPostBackControl(this.dropDownList);
            }
        }

        private void DropDownList_SelectedIndexChanged(object sender, EventArgs e)
        {
            this.selectedIndexChanged = true;
        }

        private void WebPartManager_ConnectionsActivated(object sender, EventArgs e)
        {
            if (this.selectedIndexChanged)
            {
                this.OnColorChanged(EventArgs.Empty);
            }
        }

        private void ModelProvider_ModelChanged(object sender, EventArgs e)
        {
            IModelProvider provider = (IModelProvider)sender;
            this.dropDownList.DataSource = this.GetColorsByModelId(provider.ModelId);
            this.dropDownList.DataBind();
            this.dropDownList.SelectedIndex = 0;

            this.OnColorChanged(EventArgs.Empty);

            this.updatePanel.Update();
        }
    }
}
Published 23 January 2009 10:56 by Remco.Blok

Comments

No Comments
Anonymous comments are disabled
Powered by Community Server (Personal Edition), by Telligent Systems