Using the EditorPart to Create a Custom ListView Web Part
Today I want to illustrate how to create a web part for displaying any lists content within a farm and how to make configuration as easy as possible for the users by utilizing the EditorPart class from the System.Web.UI.WebControls.WebParts namespace. While some tutorials exist on the Internet many developers have some trouble to leverage the functionality of this class, particularly when using it with MOSS 2007.
Note: This is a Visual Studio 2008 Solution, get Microsoft Visual Studio 2008 Express from here.
What are we trying to achieve?
We need a web part that satisfies the following requirements:
- Display a list’s content from any web within the farm (no webservices involved here).
- Use an EditorPart added to the web part’s EditorPartCollection which allows users to easily specify the web to display a list’s content of, to select a list from a dynamically populated DropDownList and to specify a list’s view from a DropDownList.
- Additionally, we want to be able to filter the displayed content by either giving an Eq or Contains filter expression for a single column. (We could easily filter by multiple columns but we do not want to make this exercise more complex as it already is.)
The following Screenshot illustrates the results:
Why should we use the EditorPart
Everyone who has ever developed a simple web part exposed its configurable settings through properties augmented with attributes to describe the setting’s meaning and its storage class. As long as you stick to simple properties like strings, integers, boolean expressions and enumerations you’re fine with the web part’s out-of-the-box capabilities.
Quickly you realize that if you need to add custom DropDownLists or other non-trivial controls to the ToolPane you would need something else: an EditorPart. It’s like a web part within a web part and can be populated with as many and complex controls as desired, thus making the perfect choice for our requirements.
Requirement 1: Displaying a lists content
WSS 3.0 and MOSS 2007 suffer from, in my oppinion, a major design flaw: a list, user created or not, can only be reused within the same web. By default, it is not available in subwebs or superwebs. Whenever one creates a list in SharePoint a web part for this list is created on the fly allowing to include the list multiple times just like adding web parts. Unfortunately, all the carefully accumulated data is isolated to the current web site.
Experienced SharePoint developers know that the ListViewByQuery control comes to the rescue. We can quickly hack a web part that fetches a lists content using code similar to that:
private ListViewByQuery GetListView(SPList myList, SPView myView) { SPQuery query = new SPQuery(myView); query.Query = queryText; // custom filter expression goes here query.ItemIdQuery = true; query.IncludeMandatoryColumns = true; ListViewByQuery lvbq = new ListViewByQuery(); lvbq.List = myList; lvbq.Query = query; return lvbq; } protected override void Render(HtmlTextWriter writer) { EnsureChildControls(); try { RenderChildren(writer); } catch (Exception exception) { writer.WriteLine(exception.Message); } } protected override void CreateChildControls() { try { base.CreateChildControls(); this.Controls.Add(GetListView(this.list, this.view)); } catch (Exception exception) { // handle errors } }
While that would basically do it our requirements list explicitly states that the web part must
display a list’s content from any web within the farm
This one is not difficult either. All we need is the following information:
- The URL of the site the list is located in
- The lists name
- A view (can use default view of none is given)
Knowing that we could code the following functions to instantiate an SPWeb, SPList and SPView class:
// let's assume these are available as properties private string listWebUrl; private SPWeb listWeb; private SPList list; private SPView view; private bool EnsureView() { if (this.view != null) return true; if (this.EnsureList()) { if (String.IsNullOrEmpty(this.ViewName)) { this.view = this.list.DefaultView; } else { try { this.view = this.list.Views[this.ViewName]; } catch (Exception exception) { customValidator.ErrorMessage = String.Format("The specified view '{0}' could not be found", this.viewName); customValidator.IsValid = false; } } } return this.view != null; } private bool EnsureList() { if (this.list != null) return true; if (this.EnsureWeb()) { if (String.IsNullOrEmpty(this.ListName)) { customValidator.ErrorMessage = String.Format("List name not set"); return false; } else { try { this.list = this.listWeb.Lists[this.ListName]; } catch (Exception exception) { customValidator.ErrorMessage = String.Format("The specified list '{0}' could not be found", this.ListName); customValidator.IsValid = false; } } } return this.list != null; } private bool EnsureWeb() { if (this.listWeb != null) return true; if (String.IsNullOrEmpty(this.ListWebUrl)) { this.listWeb = SPContext.Current.Web; } else { try { Uri uri = null; // test for complete URI specification in this.listWebUrl try { uri = new Uri(this.ListWebUrl); } catch (Exception exception) { uri = new Uri(SPContext.Current.Site.RootWeb.Url + this.ListWebUrl); } this.listWeb = new SPSite(uri.ToString()).OpenWeb(); } catch (Exception exception) { customValidator.ErrorMessage = String.Format("There is no web at URL '{0}'", this.ListWebUrl); customValidator.IsValid = false; } } return this.listWeb != null; }
OK, so far so good. With code like that it should be possible to fulfill requirement 1. While the code may be fine we will not use it as is. It just illustrates how we can get facilitate the WSS 3.0 object model to achieve what we are looking for. The solution presented in this article is more sophisticated.
Requirement 2: Use an EditorPane for web part configuration
The EditorPart is a control that can be augmented with additional controls just as we did previously by adding a ListViewByQuery instance to the web part calling the Add method of the web part's Controls collection.
Using the EditorPart class
An EditorPart instance will serve as a canvas for our DropDownLists and other controls needed to fulfill requirement 2. Once it has been populated with the controls we have to add it to the web part's EditorPartCollection which is achieved by overriding the WebPart's CreateEditorParts method as shown in the following code snippet:
public class CustomListViewWebPart : WebPart { public override EditorPartCollection CreateEditorParts() { ArrayList editorArray = new ArrayList(); if (this.EffectiveStorage == Microsoft.SharePoint.WebPartPages.Storage.Shared) { CustomListViewEditorPart editorPart = new CustomListViewEditorPart(); editorPart.ID = this.ID + "_customEditorPart"; editorArray.Add(editorPart); } EditorPartCollection editorParts = new EditorPartCollection(editorArray); return editorParts; } }
In order to create an EditorPart instance we first have to derive from the EditorPart class and override the usual suspects plus two additional methods adding to the magic:
- CreateChildControls - Called by the ASP.NET page framework to notify server controls that use composition-based implementation to create any child controls they contain in preparation for posting back or rendering.
- RenderContents - Renders the contents of the control to the specified writer
- SyncChanges - Retrieves the property values from a WebPart control for its associated EditorPart control
- ApplyChanges - Saves the values in an EditorPart control to the corresponding properties in the associated WebPart control
Both SyncChanges and ApplyChanges raise many questions among new and experienced developers alike. Basically, these mothods are responsible for establishing two-way communication between the web part's tool pane and the embedded EditorPart. All settings are persisted with an associated web part ID. It is therefore imparative that all settings made in the EditorPart are transfered to the web part.
The EditorPart class has a WebPartToEdit property which points to the parent web part whose properties are persisted to the database. Invoking SyncChanges causes the EditorPart to receive the parent's properties:
public override void SyncChanges() { CustomListViewWebPart parentWebPart = this.WebPartToEdit as CustomListViewWebPart; if (parentWebPart != null) { this.anEditorPartProperty = parentWebPart.aParentPropertyValue; this.someOtherProperty = parentWebPart.someOtherParentPropertyValue; } }
As we can see casting the WebPartToEdit property we gain access to the parent web part's properties and can sync its values to the EditorPart's properties.
The EditorPart's ApplyChanges method works the other way round: it transfers the property values assigned to the EditorPart instance to its parent web part:
public override void SyncChanges() { CustomListViewWebPart parentWebPart = this.WebPartToEdit as CustomListViewWebPart; parentWebPart.aParentPropertyValue = this.anEditorPartProperty; parentWebPart.someOtherParentPropertyValue = this.someOtherProperty; }
And that's all about it.
Populating the EditorPart
I will not go into details on how to add controls to a web control but there are some points worth being mentioned. First of all we never use user controls (.ASCX files) to populate the EditorPart. This is a big no-no! Second, the Open button and the Lists DropDownList are associated event handler that retrieve the appropriate data based on the selection. For instance, the event handler for the Open button iterates through all the lists in the specified list and adds their names to the Lists DropDownList:
void btnOpenWeb_Click(object sender, EventArgs e) { this.SelectedWebUrl = this.txtWebUrl.Text; PopulateLists(); } private void PopulateLists() { ddlLists.Items.Clear(); ddlFieldName.Items.Clear(); ddlViews.Items.Clear(); ListItem newItem; if (SelectedWeb == null) { ddlLists.Enabled = false; ddlViews.Enabled = false; rblIsEqual.Enabled = false; ddlFieldName.Enabled = false; return; } else { ddlLists.Enabled = true; ddlViews.Enabled = true; rblIsEqual.Enabled = true; ddlFieldName.Enabled = true; } foreach (SPList item in this.SelectedWeb.Lists) { if (item.Hidden == false) { newItem = new ListItem(); newItem.Text = item.Title; newItem.Value = item.ID.ToString(); if (this.SelectedListId == item.ID) { newItem.Selected = true; } ddlLists.Items.Add(newItem); } } if (ddlLists.SelectedItem == null) { newItem = new ListItem(); newItem.Text = String.Empty; newItem.Value = String.Empty; newItem.Selected = true; ddlLists.Items.Add(newItem); } else { OnListsChange(ddlLists, new EventArgs()); if (SelectedListViewId != Guid.Empty) { ListItem liSelected = ddlViews.Items.FindByValue(SelectedListViewId.ToString()); if (liSelected != null) { liSelected.Selected = true; } } if (SelectedFieldName != String.Empty) { ListItem liSelected = ddlFieldName.Items.FindByValue(SelectedFieldName); if (liSelected != null) { liSelected.Selected = true; } } } }
Since we use ViewStates to make things easier for us we can determine the selected item after a postback. If no item has been selected previously we just add each item to the control. Otherwise we fire call OnListChange (the event handler attached to the Lists DropDownList) to cause the EditorPart to load the selected list's views and fields:
private void OnListsChange(object sender, EventArgs e) { ddlViews.Items.Clear(); DropDownList ddlCurrent = sender as DropDownList; if (ddlCurrent != null && ddlCurrent.SelectedValue != String.Empty) { SelectedListId = new Guid(ddlCurrent.SelectedValue); SPList currentList = SelectedList; foreach (SPView view in currentList.Views) { if (view.Hidden == false) { ListItem listItem = new ListItem(); listItem.Text = view.Title; listItem.Value = view.ID.ToString(); ddlViews.Items.Add(listItem); } } ddlFieldName.Items.Clear(); foreach (SPField field in currentList.Fields) { if (field.Hidden == false && field.Group != "_Hidden" && field.TypeAsString != "Computed") { ddlFieldName.Items.Add(new ListItem(field.Title, field.InternalName)); } } ddlViews.Enabled = true; } else { ddlViews.Enabled = false; } }
Requirement 3: Allow filtering the returned list entries
As we can see from the EditorPart screenshot controls for selecting a filter method, specifying a filter value and a column to filter for have been added to fulfill requirement 3. Let's take a look behind the scenes to understand how we can do that programmatically.
Recall from requirement 1 that we can easily create a ListViewByQuery object by simply creating an SPQuery object which is based on a list's view and assigning an SPList instance to the ListViewByQuery's List property. The SPQuery instance is most interesting for us.
We can create an instance of that class by passing an SPView instance as a constructor argument. Each view may contain a CAML query determining the amount of information returned. If a view has a query string it is either of type Where, OrderBy or a both. In order to apply custom filtering the view's query string must be injected with a custom query string.
To keep things simple, we want to allow filtering for Eq or Contains expressions only. That said we can use the following algorithm to transform the view's query into a custom filter string:
- If a filter value is given:
- If the Eq expression has been selected prepare the CAML query:
- If the Contains expression has been selected:
- If the view's query has a Where expression inject the custom CAML string with an And statement
- Otherwise it has may have an order, thus the custom CAML text can be simple appended to the view's query
- If no filter value has been specified: use view's query.
queryText = String.Format("<Eq><FieldRef Name=\"{0}\" /><Value Type=\"{1}\">{2}</Value></Eq>", fieldName, fieldType, this.filterValue );
queryText = String.Format("<Contains><FieldRef Name=\"{0}\" /><Value Type=\"{1}\">{2}</Value></Contains>", fieldName, fieldType, this.filterValue );
This simple algorithm is implemented in the GetQuery method:
private SPQuery GetQuery() { SPQuery theQuery = new SPQuery(this.SelectedView); string fieldName = this.SelectedField != null ? this.SelectedField.InternalName : "FileLeafRef"; string fieldType = this.SelectedField != null ? this.SelectedField.TypeAsString : "File"; string queryText = String.Empty; if (this.IsEqual) { queryText = String.Format("<Eq><FieldRef Name=\"{0}\" /><Value Type=\"{1}\">{2}</Value></Eq>", fieldName, fieldType, this.filterValue ); } else { queryText = String.Format("<Contains><FieldRef Name=\"{0}\" /><Value Type=\"{1}\">{2}</Value></Contains>", fieldName, fieldType, this.filterValue ); } string originalQuery = SelectedView.Query; string neqQuery = String.Empty; if (originalQuery.Length > 1) { if (originalQuery.IndexOf("<Where>") == -1) { neqQuery = originalQuery + "<Where>" + queryText + "</Where>"; } else { string temp1 = originalQuery.Substring(0, originalQuery.IndexOf("<Where>") + 7); string temp2 = originalQuery.Substring(originalQuery.IndexOf("<Where>") + 7); neqQuery = temp1 + "<And>" + temp2; temp1 = neqQuery.Substring(0, neqQuery.IndexOf("</Where>")); temp2 = neqQuery.Substring(neqQuery.IndexOf("</Where>")); neqQuery = temp1 + queryText + "</And>" + temp2; } } else { neqQuery = originalQuery + "<Where>" + queryText + "</Where>"; } theQuery.Query = neqQuery; return theQuery; }
Once an appropriate SPQuery instance has been created it is assigned to the ListViewByQuery object resulting in the desired effect.
Conclusion
In this post we have seen how to create a web part that is capable of displaying a lists content from any web site within the farm and how to utilize the ToolPart class to make things as easy for users as possible.
In a another post I will demonstrate how we can apply sorting and user filtering and how to add a fully fledged toolbar to the ListViewByQuery just as users are used to have with the real thing.
Thanks for reading.
Note: This is a Visual Studio 2008 Solution, get Microsoft Visual Studio 2008 Express from here.


