SharePoint 2010 – Ajax Panel in Web Part’s Editor Part

June 28 2010 10 comments

Have you ever wanted to have conditional property setting possibilities in your custom web parts? I have. The scenario has been that I would have wanted to render a control to web part property setting based on some other property selection – i.e. if user selects something from web part’s properties, something else would emerge to be selected from or filled in. When developing over SharePoint 2007 I had an idea how to do it with Ajax but never had the time to make it happen as in SharePoint 2007 you wouldn’t have ASP.NET 3.5 and Ajax web server extensions available in your web application by default.

In SharePoint 2010 you don’t have that limitation any more so I gave my thought a go and below is a sample what I came up with – a web part with one drop down list, two multi selectable list boxes and one text field in the web part’s editor part. Web template is chosen from the drop down list. The first list box makes all site collection’s root webs of a web application available to be selected from and the other makes a list of sites published with a selected web template. The text box contains an integer value.

Firstly, here’s the web part’s property definitions and editor part definition:

[ToolboxItemAttribute(false)]
public class DemoWebPart : WebPart
{
    [WebBrowsable(false), Personalizable(PersonalizationScope.Shared)]
    public List<string> RootWebs { get; set; }

    [WebBrowsable(false), Personalizable(PersonalizationScope.Shared)]
    public List<string> Webs { get; set; }

    private string _webTemplate = "BLOG#0";
    [WebBrowsable(false), Personalizable(PersonalizationScope.Shared)]
    public string WebTemplate
    {
        get { return _webTemplate; }
        set { _webTemplate = value; }
    }

    private int _limit = 5;
    [WebBrowsable(false), Personalizable(PersonalizationScope.Shared)]
    public int Limit { get { return _limit; } set { _limit = value; } }

    public override EditorPartCollection CreateEditorParts()
    {
        var editor = new DemoEditorpart
        {
            ID = ID + "_Editor",
            Title = "Demo Editor Part"
        };
        return new EditorPartCollection(new EditorPart[] { editor });
    }
}

Secondly, here’s the Editor Part implementation:

public class DemoEditorpart : EditorPart
{
    // list box to select site collection's root webs from
    protected ListBox Sites;
    // list box to select webs from
    protected ListBox Webs;
    protected DropDownList WebTemplates;
    protected TextBox Limit;

    protected override void CreateChildControls()
    {

        Limit = new TextBox { CssClass = "UserInput", Width = 20 };
        // autopostback
        WebTemplates = new DropDownList
        {
            CssClass = "UserInput",
            Width = 176,
            ID = ID + "WebTemplates",
            AutoPostBack = true
        };
        Sites = new ListBox
        {
            CssClass = "UserInput",
            Width = 176,
            ID = ID + "Sites",
            SelectionMode = ListSelectionMode.Multiple,
            AutoPostBack = true
        };
        Webs = new ListBox
        {
            CssClass = "UserInput",
            Width = 176,
            ID = ID + "Webs",
            SelectionMode = ListSelectionMode.Multiple
        };

        var webTemplates = SPContext.Current.Site.GetWebTemplates(1033);
        WebTemplates.DataSource = webTemplates;
        WebTemplates.DataTextField = "Title";
        WebTemplates.DataValueField = "Name";
        WebTemplates.DataBind();
        WebTemplates.SelectedIndexChanged += WebTemplates_SelectedIndexChanged;

        // get web applications site collection root webs to display
        var sites = GetRootWebs();
        if (sites != null)
        {
            Sites.DataSource = sites;
            Sites.DataTextField = "Title";
            Sites.DataValueField = "Url";
            Sites.Rows = sites.Count + 2;
            Sites.DataBind();
            Sites.SelectedIndexChanged += Sites_SelectedIndexChanged;
        }
        var panel = new UpdatePanel { ID = ID + "UpdatePanel" };
        panel.ContentTemplateContainer.Controls.Add(
            new LiteralControl(@"<table cellspacing=""0"" border=""0""
                style="
"border-width:0px;width:100%;
                border-collapse:collapse;"
"><tr><td>
                <div class="
"UserSectionHead"">"));

        panel.ContentTemplateContainer.Controls.Add(
            new LiteralControl(@"Site Template:<br/>"));
        panel.ContentTemplateContainer.Controls.Add(WebTemplates);
        panel.ContentTemplateContainer.Controls.Add(
            new LiteralControl(@"</div></td></tr>
                <tr><td>
                <div style="
"width:100%"" class=""UserDottedLine"">
                </div></td></tr><tr><td>
                <div class="
"UserSectionHead"">"));


        panel.ContentTemplateContainer.Controls.Add(
            new LiteralControl(@"Site Collections:<br/>"));
        panel.ContentTemplateContainer.Controls.Add(Sites);
        panel.ContentTemplateContainer.Controls.Add(
            new LiteralControl(@"</div></td></tr>
                <tr><td>
                <div style="
"width:100%"" class=""UserDottedLine"">
                </div></td></tr><tr><td>
                <div class="
"UserSectionHead"">"));

        panel.ContentTemplateContainer.Controls.Add(
            new LiteralControl("Sites:<br/>"));
        panel.ContentTemplateContainer.Controls.Add(Webs);

        panel.ContentTemplateContainer.Controls.Add(new LiteralControl(@"<tr><td><div
        class="
"UserSectionHead"">Limit:<br/>"));
        panel.ContentTemplateContainer.Controls.Add(Limit);
        panel.ContentTemplateContainer.Controls.Add(
            new LiteralControl(@"</div></td></tr><tr><td>
            <div style="
"width:100%"" class=""UserDottedLine""></div>
            </td></tr></table>"
));

        Controls.Add(panel);
        base.CreateChildControls();

    }

    /// <summary>
    /// event handler to list webs under the selected site collection
    /// with the selected web template
    /// event handler is the same as below - could use the same
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    protected void WebTemplates_SelectedIndexChanged(object sender, EventArgs e)
    {
        FillWebs(Sites.Items, Webs.Items);
        Webs.Rows = Webs.Items.Count + 2;
    }


    /// <summary>
    /// event handler to list webs under the selected site collection
    /// with the selected web template
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    protected void Sites_SelectedIndexChanged(object sender, EventArgs e)
    {
        FillWebs(Sites.Items, Webs.Items);
        Webs.Rows = Webs.Items.Count + 2;
    }

    /// <summary>
    /// fill the Webs list box according to the selected site collection
    /// </summary>
    /// <param name="sites">site collections</param>
    /// <param name="webs">webs</param>
    private void FillWebs(ListItemCollection sites,
        ListItemCollection webs)
    {
        webs.Clear();
        foreach (ListItem item in sites)
        {
            var newsWebs = GetWebs(item.Value);
            foreach (var web in newsWebs)
            {
                var listItem = new
                    ListItem(web.Title,
                    item.Value + ";" + web.ServerRelativeUrl);
                if (item.Selected && !webs.Contains(listItem))
                {
                    webs.Add(listItem);
                }
                else if (!item.Selected && webs.Contains(listItem))
                {
                    webs.Remove(listItem);
                }
            }
        }
    }

    /// <summary>
    /// apply changes to web part
    /// </summary>
    /// <returns></returns>
    public override bool ApplyChanges()
    {
        var part = (DemoWebPart)WebPartToEdit;
        // apply values back to web part from editor part
        int result;
        if (int.TryParse(Limit.Text, out result))
        {
            part.Limit = result;
        }
        part.WebTemplate = WebTemplates.SelectedValue;
        part.RootWebs = ApplyRootWebs(Sites.Items);
        part.Webs = ApplyWebs(Webs.Items);
        return true;
    }

    /// <summary>
    /// sync changes from web part
    /// </summary>
    public override void SyncChanges()
    {
        EnsureChildControls();
        var part = (DemoWebPart)WebPartToEdit;
        // sync values from web part to editor part
        Limit.Text = part.Limit.ToString();
        WebTemplates.SelectedValue = part.WebTemplate;
        SyncRootWebs(Sites.Items, part.RootWebs);
        SyncWebs(Sites.Items, Webs.Items, part.Webs);
        Webs.Rows = Webs.Items.Count + 2;
    }

    /// <summary>
    /// apply selected site collections to web part
    /// </summary>
    /// <param name="listItems">Sites items</param>
    /// <returns>list of selected site collections</returns>
    private static List<string> ApplyRootWebs(IEnumerable listItems)
    {
        return (from ListItem listItem in listItems
                where listItem.Selected
                select string.Format("{0};{1};{2}", listItem.Text,
                listItem.Value, string.Empty)).ToList();
    }

    /// <summary>
    /// apply selected webs to web part
    /// </summary>
    /// <param name="listItems">Webs items</param>
    /// <returns>list of seleted webs</returns>
    private static List<string> ApplyWebs(IEnumerable listItems)
    {
        return (from ListItem listItem in listItems
                where listItem.Selected
                select string.Format("{0};{1};{2}", listItem.Text,
                listItem.Value.Split(';')[0],
                listItem.Value.Split(';')[1])).ToList();
    }

    /// <summary>
    /// sync site collections from web part
    /// </summary>
    /// <param name="rootWebItems">Sites items</param>
    /// <param name="rootWebs">selecetd site collections from web part</param>
    private static void SyncRootWebs(ListItemCollection rootWebItems,
        IEnumerable<string> rootWebs)
    {
        if (rootWebs == null) return;

        foreach (var siteInfo in rootWebs.Select(rootWeb =>
            rootWeb.Split(';')))
        {
            var rootWebItem = rootWebItems.FindByValue(siteInfo[1]);
            if (rootWebItem != null) rootWebItem.Selected = true;
        }
    }

    /// <summary>
    /// sync webs from web part
    /// </summary>
    /// <param name="rootWebItems">Sites items</param>
    /// <param name="webItems">Webs items</param>
    /// <param name="webs">selected webs from web part</param>
    private void SyncWebs(ListItemCollection rootWebItems,
        ListItemCollection webItems,
        IEnumerable<string> webs)
    {
        if (webs == null) return;

        FillWebs(rootWebItems, webItems);

        foreach (var siteInfo in webs.Select(web => web.Split(';')))
        {
            var webItemValue = string.Format("{0};{1}", siteInfo[1],
                siteInfo[2]);
            var webItem = webItems.FindByValue(webItemValue);
            if (webItem != null) webItem.Selected = true;
        }
    }

    /// <summary>
    /// get every root web from web application - display also the
    /// names of those site collections user has no right to
    /// </summary>
    /// <returns>list of webs</returns>
    private List<SPWeb> GetRootWebs()
    {
        var webs = new List<SPWeb>();

        SPSecurity.RunWithElevatedPrivileges(delegate
        {
            foreach (SPSite site in
                    SPContext.Current.Site.WebApplication.Sites)
            {
                webs.Add(site.RootWeb);
            }
        });

        return webs;
    }

    /// <summary>
    /// get sub webs published with a certain template
    /// </summary>
    /// <param name="siteName">site name</param>
    /// <returns>list of webs</returns>
    private IEnumerable<SPWeb> GetWebs(string siteName)
    {
        SPSite site = null;
        SPSecurity.RunWithElevatedPrivileges(delegate
        {
            site = SPContext.Current.Site.WebApplication.Sites[siteName];
        });
        return GetWebs(site ?? SPContext.Current.Site);
    }

    /// <summary>
    /// get sub webs published with a certain template
    /// </summary>
    /// <param name="site">site</param>
    /// <returns>list of webs</returns>
    private IEnumerable<SPWeb> GetWebs(SPSite site)
    {
        var webTemplate = WebTemplates.SelectedValue;

        // SPWeb doesn't contain the same info as SPWebTemplate.Name
        // SPWebTemplate.Name contains "TEMPLATE#N"
        // where SPWeb WebTemplate contains only the "TEMPLATE"
        return site.RootWeb.GetSubwebsForCurrentUser().Where(w =>
            webTemplate.StartsWith(w.WebTemplate)).ToList();
    }
}

The result is that we can select site collections from web part properties and the underlying webs selection is filled dynamically causing post back within the Ajax panel in the editor part. Also the web template -selection causes a post back to refresh the Webs list box to match webs found created with the selected web template. The tricky part here was to get the editor part sync properties from the web part and apply changes back to the web part and to handle the post backs caused in the panel. Below are two clips of how the property setting works:

Select site web template

Web Template is available to be selected from.

Sites under the selected site collections published with the selected web template are available to be selected from.

Sites under the selected site collections published with the selected web template are available to be selected from.

Other sites created with the selected web template are also available to be selected from.

Other sites created with the selected web template are also available to be selected from.

Popularity: 3% [?]

10 comments to “SharePoint 2010 – Ajax Panel in Web Part’s Editor Part”

  1. [...] SharePoint 2010 – Ajax Panel in Web Part’s Editor Part (SharePoint Blues)Have you ever wanted to have conditional property setting possibilities in your custom web parts? I have. The scenario has been that I would have wanted to render a control to web part property setting based on some other property selection – i.e. if user selects something from web part’s properties, something else would emerge to be selected from or filled in. When developing over SharePoint 2007 I had an idea how to do it with Ajax but never had the time to make it happen as in SharePoint 2007 you wouldn’t have ASP.NET 3.5 and Ajax web server extensions available in your web application by default. [...]

  2. [...] SharePoint 2010 – Ajax Panel in Web Part’s Editor Part (SharePoint Blues)Have you ever wanted to have conditional property setting possibilities in your custom web parts? I have. The scenario has been that I would have wanted to render a control to web part property setting based on some other property selection – i.e. if user selects something from web part’s properties, something else would emerge to be selected from or filled in. When developing over SharePoint 2007 I had an idea how to do it with Ajax but never had the time to make it happen as in SharePoint 2007 you wouldn’t have ASP.NET 3.5 and Ajax web server extensions available in your web application by default. [...]

  3. Excellent work, well done fella!

  4. [...] the current site collection when running code in SharePoint. I’ve also written about using Ajax panel in web parts editor part (this post is slightly edited to serve this one). I’m going to take these two posts as basis [...]

  5. Abu Hamzah says:

    thanks i am working on something similar with two dropdownlist, where can i find to download the project for the above one?

    thanks.

  6. Abu Hamzah says:

    i started copying and pasting the code and run it and i able to see the custom editor part but when i change the Site Template the Site Collections listbox is not changing it just showing the 4 options i get and regardless what i select from the Site Template i still those 4 options…. looks like the AutoPost is not firing?

  7. Juha Pitkänen says:

    Hi Abu, I can’t tell what could be wrong in your solution but if you try and debug it you might find the problem and see if the autopostback is firing. The screenshots above are created with the provided code samples and there you can see the autopostback working (the Site Collections and Sites ListBoxes are updated). But it has been a year and a half and I remember editing the post after first publishing it, so if you find something wrong with it, let me know.

  8. George Durzi says:

    Do you have any suggestion for maintaining some sort of state in the EditorPart? For example, I am adding a LiteralControl and setting it’s text to some HTML markup. The markup is styled so that the jQueryUI Sortable plugin will run on it. I want to capture the new markup after hitting OK/Apply, but trying to get the Text property of the LiteralControl triggers CreateChildControls to run again which sets it to its old value.
    Thanks!

  9. I feel that is one of the such a lot important info for me. And i’m happy studying your article. But want to statement on some general things, The website taste is ideal, the articles is in reality excellent : D. Good process, cheers

  10. Marisa says:

    It’s going to be finish of mine day, except before finish I am reading this
    enormous article to improve my knowledge.

Leave a Reply