Custom WebPart Editor

One of the nice things about a webpart is it's flexibility! With a webpart, you can expose a bunch of properties to allow the user to quickly change the behavior and look of the webpart. This post will walk you through how to create a custom webpart editor with dynamic properties using an EditorPart. I will only cover how to load and save these properties.

If you want to just add simple custom properties (with user entered values) that aren't dynamically populated you don't need to create a custom webpart editor using the EditorPart!

Whenever I create a webpart, I typically add the following properties:

  • Site Url
  • List
  • View

With these properties, the user has the flexibily to point the webpart to any list in the site collection (not limited to just the current site), and can use the view as a filter for the data (not limited to logic in the webpart). For example, if you wanted the webpart to only display archived items, you simply have to create a view to reflect this and update the webpart's view proprty. There is no code change required!

The SiteUrl will be a textbox, while the List and View properties will be dropdowns... this will provide a better user experience, as it allows the user to simply select from a list of possible values, instead of having them type things in, which could lead to user error.

 

WebPart-editor

Ok so let's begin our custom webpart editor! We will start with a new SharePoint 2013 - Visual Web Part project.

1. WebPart: MyVisualWebPart.ascx.cs

First thing we need to do is implement the IWebEditable interface, this allows us to specify custom editing controls that are associate with a webpart control:

[code language="csharp"]
public partial class MyVisualWebPart : WebPart, IWebEditable
[/code]

With this we will need to implement the CreateEditorParts() function and the WebBrowsableObject property:

[code language="csharp"]
EditorPartCollection IWebEditable.CreateEditorParts()
{
List editors = new List();
editors.Add(new HtmlEditor()); // We will create the HtmlEditor

return new EditorPartCollection(editors);
}

object IWebEditable.WebBrowsableObject
{
get { return this; }
}
[/code]

The above steps will always be the same for any webpart you build... The only thing that changes between webparts in this file is the webpart properties. In our example we will define the following three properties:

[code language="csharp"]
public string SiteUrl
{
get;
set;
}

public string List
{
get;
set;
}

public string View
{
get;
set;
}
[/code]

The HtmlEditor class above (in the CreateEditorParts) is the custom webpart editor that we will be creating! This is what your file should look like with all the changes:

[code language="csharp"]
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Web.UI.WebControls.WebParts;

namespace MyVisualWebPartProject.VisualWebPart
{
[ToolboxItemAttribute(false)]
public partial class MyVisualWebPart : WebPart, IWebEditable
{
public string SiteUrl
{
get;
set;
}

public string List
{
get;
set;
}

public string View
{
get;
set;
}

public MyVisualWebPart()
{
}

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

protected void Page_Load(object sender, EventArgs e)
{
}

EditorPartCollection IWebEditable.CreateEditorParts()
{
List editors = new List();
editors.Add(new HtmlEditor());

return new EditorPartCollection(editors);
}

object IWebEditable.WebBrowsableObject
{
get { return this; }
}
}
}
[/code]

2. Custom WebPart Editor: HtmlEditor.cs

This is the custom webpart editor that we will create, which will add a custom section to the webpart properties! This is a nice and professional way of adding your properties: in it's own collapsible section of the webpart! Right click on your VisualWebPart and select Add -> Class... and name it HtmlEditor.cs.

There are a couple of important things about this class, the rest is basically just setting up the controls, which I will skip over. The first thing to note is that this class needs to inherit EditorPart, so that we can add it to the list of EditorParts which we did in the previous step. Then there are 4 methods that we need to override: CreateChildControls(), RenderContents(), ApplyChanges() and SyncChanges().

CreateChildControls and RenderContents: does just that... creates your controls and renders them in the custom editor section. This is where you can add all your controls, like textboxes, checkboxes, dropdowns and so on... The values of these controls will be stored in the properties created in the VisualWebPart. In our example we will have a textbox for the SiteUrl property and two dropdowns for the List and View properties.

ApplyChanges: this method takes the values from the controls and applies them to the properties in the VisualWebPart. So when you click on OK or Cancel, this method is executed.

SyncChanges: this method takes the saved VisualWebPart properties and populates the controls in the custom editor. When you click on Edit Web Part on the page and the properties are shown, this method is executed.

The first thing to do is add the necessary controls. I personally like to use two Panels, one for the custom editor section, and one where all the controls will be added to. I also like to create a global Literal which will be re-used over and over again. I wont really go through the details of this code, as it is pretty self explanatory:

[code language="csharp"]
protected override void CreateChildControls()
{
this.Title = "My Visual WebPart Properties";

Controls.Clear();

this.pnlHtmlEditor = new Panel();
this.pnlHtmlEditor.CssClass = "ms-ToolPartSpacing";
this.Controls.Add(pnlHtmlEditor);

this.pnlMain = new Panel();

// Create the Site Url textbox
// Subscribe to the TextChanged event so that we can populate the Lists dropdown based on the Site URL provided
this.literal = new Literal();
this.literal.Text = "</pre>
<div class="UserSectionHead">Site URL</div>
<div class="UserSectionBody">
<div class="UserControlGroup">";
this.pnlMain.Controls.Add(this.literal);
this.tbSiteUrl = new TextBox();
this.tbSiteUrl.AutoPostBack = true;
this.tbSiteUrl.TextChanged += SiteUrlTextChanged;
this.pnlMain.Controls.Add(this.tbSiteUrl);
this.literal = new Literal();
this.literal.Text = "</div>
</div>
<pre>
";
this.pnlMain.Controls.Add(literal);

// Create the Lists dropdown
// Subscribe to the SelectedIndexChanged event so that we can populate the Views dropdown based on the selected List
// Initialize the dropdown with values matching the SiteUrl
this.ddlLists = new DropDownList();
this.ddlLists.AutoPostBack = true;
this.ddlLists.SelectedIndexChanged += ListSelectedIndexChanged;
this.ddlLists.CssClass = "UserInput";
this.ddlLists.Width = new Unit("175px", CultureInfo.InvariantCulture);
InitializeFilteredListDropDown();
this.literal = new Literal();
this.literal.Text = "</pre>
<div class="UserSectionHead">List</div>
<div class="UserSectionBody">
<div class="UserControlGroup">";
this.pnlMain.Controls.Add(this.literal);
this.pnlMain.Controls.Add(this.ddlLists);
this.literal = new Literal();
this.literal.Text = "</div>
</div>
<pre>
";
this.pnlMain.Controls.Add(literal);

// Create the Views dropdown
// Initialize the dropdown with values matching the selected List
this.ddlViews = new DropDownList();
this.ddlViews.CssClass = "UserInput";
this.ddlViews.Width = new Unit("175px", CultureInfo.InvariantCulture);
InitializeViewsDropdown();
this.literal = new Literal();
this.literal.Text = "</pre>
<div class="UserSectionHead">View</div>
<div class="UserSectionBody">
<div class="UserControlGroudp">";
this.pnlMain.Controls.Add(this.literal);
this.pnlMain.Controls.Add(this.ddlViews);
this.literal = new Literal();
this.literal.Text = "</div>
</div>
<pre>
";
this.pnlMain.Controls.Add(literal);

pnlHtmlEditor.Controls.Add(pnlMain);
}
[/code]

Now that we have placed our controls the way we like, we need to render them! This is a simple one liner, simply render the main panel:

[code language="csharp"]
protected override void RenderContents(HtmlTextWriter writer)
{
pnlMain.RenderControl(writer);
}
[/code]

The final steps are to save and load the properties! To do this we need to get a handle to our actual VisualWebPart in order to access it's properties. This is where the previous section comes into play! All we do here is that if we are closing the webpart editor, we set the webpart's properties, and if we are loading the webpart editor we copy the webpart's properties into the contorls. Something like this:

[code language="csharp"]
public override bool ApplyChanges()
{
EnsureChildControls();
MyVisualWebPart myVisualWebPart = WebPartToEdit as MyVisualWebPart;

if (myVisualWebPart == null)
{
return false;
}

myVisualWebPart.SiteUrl = this.tbSiteUrl.Text;
myVisualWebPart.List = this.ddlLists.SelectedValue;
myVisualWebPart.View = this.ddlViews.SelectedValue;

return true;
}

public override void SyncChanges()
{
EnsureChildControls();
MyVisualWebPart myVisualWebPart = WebPartToEdit as MyVisualWebPart;

if (myVisualWebPart == null)
{
return;
}

this.tbSiteUrl.Text = myVisualWebPart.SiteUrl;
// Before selecting the dropdown value, first we must fill it
InitializeFilteredListDropDown();
this.ddlLists.SelectedValue = myVisualWebPart.List;
// Before selecting the dropdown value, fist we must fill it
InitializeViewsDropdown();
this.ddlViews.SelectedValue = myVisualWebPart.View;
}
[/code]

This is what your custom webpart editor file should look like with all the changes:

[code language="csharp"]
using Microsoft.SharePoint;
using Microsoft.SharePoint.Administration;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;

namespace MyVisualWebPartProject.VisualWebPart
{
class HtmlEditor : EditorPart
{
#region Private Member Variables
private Panel pnlHtmlEditor;
private Panel pnlMain;
private Literal literal;
private TextBox tbSiteUrl;
private DropDownList ddlLists;
private DropDownList ddlViews;
#endregion

#region Constructors
public HtmlEditor()
{
this.ID = "HtmlEditor";
}
#endregion

#region Private Methods
///
/// This method will populate the Lists DropDownList with the names of all the lists from the given Site URL
/// Filtering to only show GenerLists
///

private void InitializeFilteredListDropDown()
{
this.ddlLists.Items.Clear();
ListItem selectItem = new ListItem(" - Select List - ", string.Empty);
this.ddlLists.Items.Add(selectItem);

SPSecurity.RunWithElevatedPrivileges(delegate()
{
string siteUrl = string.IsNullOrEmpty(this.tbSiteUrl.Text) ? SPContext.Current.Web.Url : SPContext.Current.Site.Url + this.tbSiteUrl.Text;

try
{
using (SPSite site = new SPSite(siteUrl))
{
using (SPWeb web = site.OpenWeb())
{
SPListCollection allLists = web.Lists;

foreach (SPList list in allLists)
{
if (list.BaseTemplate == SPListTemplateType.GenericList)
{
ListItem item = new ListItem();
item.Text = list.Title;
item.Value = list.Title;

this.ddlLists.Items.Add(item);
}
}
}
}
}
catch (Exception ex)
{
SPDiagnosticsService.Local.WriteTrace(0, new SPDiagnosticsCategory("MyVisualWebPart", TraceSeverity.Unexpected, EventSeverity.Error), TraceSeverity.Unexpected, ex.Message, ex.StackTrace);
}
});
}

///
/// This method will populate the Views DropDownList with the names of all the views from the current selected List
///

private void InitializeViewsDropdown()
{
this.ddlViews.Items.Clear();
ListItem selectItem = new ListItem(" - Select View - ", string.Empty);
this.ddlViews.Items.Add(selectItem);

SPSecurity.RunWithElevatedPrivileges(delegate()
{
string siteUrl = string.IsNullOrEmpty(this.tbSiteUrl.Text) ? SPContext.Current.Web.Url : SPContext.Current.Site.Url + this.tbSiteUrl.Text;

try
{
using (SPSite site = new SPSite(siteUrl))
{
using (SPWeb web = site.OpenWeb())
{
SPList list = web.Lists.TryGetList(this.ddlLists.SelectedValue);

if (list != null)
{
foreach (SPView view in list.Views)
{
if (!view.Hidden)
{
ListItem item = new ListItem();
item.Text = view.Title;
item.Value = view.Title;

this.ddlViews.Items.Add(item);
}
}
}
}
}
}
catch (Exception ex)
{
SPDiagnosticsService.Local.WriteTrace(0, new SPDiagnosticsCategory("MyVisualWebPart", TraceSeverity.Unexpected, EventSeverity.Error), TraceSeverity.Unexpected, ex.Message, ex.StackTrace);
}
});
}

///
/// This event will re-populate the Views DropDownList with all the views for the current selected List
///

///
///
private void ListSelectedIndexChanged(object sender, EventArgs e)
{
InitializeViewsDropdown();
this.ddlViews.SelectedValue = string.Empty;
}

///
/// This event will re-populate the Lists DropDownList with all the lists for the current Site URL
///

///
///
private void SiteUrlTextChanged(object sender, EventArgs e)
{
InitializeFilteredListDropDown();
this.ddlLists.SelectedValue = string.Empty;
}
#endregion

#region Public Methods
///
/// Creates all the necessary controls for the Custom Web Part Editor
///

protected override void CreateChildControls()
{
this.Title = "My Visual WebPart Properties";

Controls.Clear();

this.pnlHtmlEditor = new Panel();
this.pnlHtmlEditor.CssClass = "ms-ToolPartSpacing";
this.Controls.Add(pnlHtmlEditor);

this.pnlMain = new Panel();

this.literal = new Literal();
this.literal.Text = "</pre>
<div class="UserSectionHead">Site URL</div>
<div class="UserSectionBody">
<div class="UserControlGroup">";
this.pnlMain.Controls.Add(this.literal);
this.tbSiteUrl = new TextBox();
this.tbSiteUrl.AutoPostBack = true;
this.tbSiteUrl.TextChanged += SiteUrlTextChanged;
this.pnlMain.Controls.Add(this.tbSiteUrl);
this.literal = new Literal();
this.literal.Text = "</div>
</div>
<pre>
";
this.pnlMain.Controls.Add(literal);

this.ddlLists = new DropDownList();
this.ddlLists.AutoPostBack = true;
this.ddlLists.SelectedIndexChanged += ListSelectedIndexChanged;
this.ddlLists.CssClass = "UserInput";
this.ddlLists.Width = new Unit("175px", CultureInfo.InvariantCulture);
InitializeFilteredListDropDown();
this.literal = new Literal();
this.literal.Text = "</pre>
<div class="UserSectionHead">List</div>
<div class="UserSectionBody">
<div class="UserControlGroup">";
this.pnlMain.Controls.Add(this.literal);
this.pnlMain.Controls.Add(this.ddlLists);
this.literal = new Literal();
this.literal.Text = "</div>
</div>
<pre>
";
this.pnlMain.Controls.Add(literal);

this.ddlViews = new DropDownList();
this.ddlViews.CssClass = "UserInput";
this.ddlViews.Width = new Unit("175px", CultureInfo.InvariantCulture);
InitializeViewsDropdown();
this.literal = new Literal();
this.literal.Text = "</pre>
<div class="UserSectionHead">View</div>
<div class="UserSectionBody">
<div class="UserControlGroup">";
this.pnlMain.Controls.Add(this.literal);
this.pnlMain.Controls.Add(this.ddlViews);
this.literal = new Literal();
this.literal.Text = "</div>
</div>
<pre>
";
this.pnlMain.Controls.Add(literal);

pnlHtmlEditor.Controls.Add(pnlMain);
}

///
/// Renders the Web Part Editor Part HTML
///
///
protected override void RenderContents(HtmlTextWriter writer)
{
pnlMain.RenderControl(writer);
}

///
/// Applies the changes to the Photo Gallery Web Part
///
/// Returns true if the changes applied
public override bool ApplyChanges()
{
EnsureChildControls();
MyVisualWebPart myVisualWebPart = WebPartToEdit as MyVisualWebPart;

if (myVisualWebPart == null)
{
return false;
}

myVisualWebPart.SiteUrl = this.tbSiteUrl.Text;
myVisualWebPart.List = this.ddlLists.SelectedValue;
myVisualWebPart.View = this.ddlViews.SelectedValue;

return true;
}

///
/// Syncs the values from the Photo Gallery to the Web Part Editor
///
public override void SyncChanges()
{
EnsureChildControls();
MyVisualWebPart myVisualWebPart = WebPartToEdit as MyVisualWebPart;

if (myVisualWebPart == null)
{
return;
}

this.tbSiteUrl.Text = myVisualWebPart.SiteUrl;
InitializeFilteredListDropDown();
this.ddlLists.SelectedValue = myVisualWebPart.List;
InitializeViewsDropdown();
this.ddlViews.SelectedValue = myVisualWebPart.View;
}
#endregion
}
}
[/code]

And that's how you create a custom webpart editor! The above code is fully functional, so you can simply copy and paste into your Visual Studio solution and give it a try!

It’s Time To Transform

Let us show you how much easier your work life can be with Bonzai Intranet on your team.