Improving the SharePoint User Experience with Custom Web Part Editors

Today’s blog comes courtesy of Christopher Harrison (@GeekTrainer), a Microsoft Certified Trainer (MCT), teaching mainly SharePoint and SQL Server and the owner and Head Geek at GeekTrainer Inc. Christopher has been training for the past 12+ years, with a couple of breaks in the middle to take on full time jobs. These days he focuses mainly on training, presenting at conferences, and consulting. I had the pleasure of attending a SharePoint Developer session presented by Christopher in Ottawa, and he agreed to share some insight on a SharePoint developer feature. Christopher even included his own My 5 in this blog post, in fact to give credit where credit is due, it was actually one of his posts where I first saw the My 5 concept. So a big thank you for a great blog, complete with a link at the end so you can download the code . Enjoy! Now, take it away Christopher…

Custom Web Part Editors

One of the most critical aspects to any successful SharePoint deployment is achieving a high level of user adoption. There are many factors that will aid in driving people to SharePoint, but one of the biggest is ensuring users have access to their data in their desired format and structure. This is where web parts come into play.

Web parts are of course little boxes, applets, gadgets, widgets, or whatever you like to call them, that perform some action or display some bit of data. Users love web parts because it allows them to easily create and edit pages without having to actually write any code.

But in order for a web part’s true potential to be realized, it needs to be easily edited by the user. You can basically add any property and configure it as editable by the user by adding a couple of simple attributes to a property. Consider the following web part[1]:

[WebBrowsable(true)]

[Category("Configuration")]

[WebDisplayName("Sales Person ID")]

[WebDescription("ID of the desired sales person’s orders")]

[Personalizable(PersonalizationScope.User)]

publicInt32 SalesPersonId { get; set; }

privateGridView gvRecentOrders;

privateString connectionString = //AdventureWorks connection string

privateString sql = @"SELECT TOP 10 OrderDate

, TotalDue

FROM Sales.SalesOrderHeader

WHERE SalesPersonID = @SalesPersonID

ORDER BY OrderDate DESC";

protectedoverridevoidOnPreRender(EventArgs e)

{

if(SalesPersonId == 0) {

DisplayEditLink();

} else {

DisplayRecentOrders();

}

}

privatevoid DisplayEditLink()

{

HyperLink editLink = newHyperLink();

editLink.NavigateUrl = String.Format(

"javascript:ShowToolPane2Wrapper(‘Edit’,’129′,'{0}’);"

, this.ID);

editLink.ID = String.Format("MsoFrameworkToolpartDefmsg_{0}"

, this.ID);

editLink.Text = "Click here to edit web part";

this.Controls.Add(editLink);

}

privatevoid DisplayRecentOrders()

{

gvRecentOrders = newGridView();

using (SqlConnection connection =

newSqlConnection(connectionString))

using (SqlCommand command = newSqlCommand(sql, connection)) {

connection.Open();

command.Parameters.AddWithValue

("@SalesPersonID", SalesPersonId);

using (SqlDataReader reader = command.ExecuteReader()) {

gvRecentOrders.DataSource = reader;

gvRecentOrders.DataBind();

}

}

Controls.Add(gvRecentOrders);

}

A Couple of quick notes:

First – there’s nothing overly fancy about what I’m doing. I have a property (SalesPersonID) that I want to use to filter the orders.DisplayRecentOrders()executes the query and binds the data to theGridView.

Second – I’ve taken a couple of short cuts, hard coding my SQL and my connection string. Not a good idea for production, but just fine for blog posts. 🙂

Third – I’ve included a cool little bit of boiler plate code in DisplayEditLink. This will check to see if a value has been set on SalesPersonID, and if not provide a link the user can click on to open the tool pane. That’s much more intuitive for users new to SharePoint than having to click on the little drop down arrow on the upper right corner of the web part.

Here’s the problem. When the tool pane is created, it’s going to use a default editor for SalesPersonID. Because SalesPersonId is an integer, the default editor is going to be a textbox, which would require the user to type in the ID of the desired sales person. That’s obviously not ideal. While I could create a connected web part, and allow the user to choose the sales person from there, that would mean I’d have to add another web part to the page and configure the connection, which is a bit more than I really need for a simple one value property.

The solution? Customize the editor.

SharePoint allows you to create your own editor for a web part if you are not happy with the one created for you. The first step is to create a new class that inherits from EditorPart. EditorPart is similar to a normal web control, except it has two methods that need to be implemented:

ApplyChanges() – Responsible for updating the web part with the chosen set through the editor

SyncChanges() – Responsible for updating the editor with the values from the web part

Creating the EditorPart involves overriding CreateChildControls() (like a normal control), and then overriding those two methods. The end result looks a bit like the following:

privateString connectionString = //AdventureWorks connection string

privateString sql =

@"SELECT c.FirstName + ‘ ‘ + c.LastName AS Name

, sp.SalesPersonId

FROM Person.Contact c

INNER JOIN HumanResources.Employee e

ON c.ContactID = e.ContactID

INNER JOIN Sales.SalesPerson sp

ON e.EmployeeID = sp.SalesPersonID

ORDER BY Name";

privateDropDownList ddlSalesPeople;

privateLabel lblSalesPeople;

protectedoverridevoid CreateChildControls()

{

this.Title = "Recent Sales Properties";

lblSalesPeople = newLabel();

lblSalesPeople.Text = "<b>Sales Person:</b>";

Controls.Add(lblSalesPeople);

ddlSalesPeople = newDropDownList();

ddlSalesPeople.DataValueField = "SalesPersonId";

ddlSalesPeople.DataTextField = "Name";

using(SqlConnection connection =

newSqlConnection(connectionString))

using(SqlCommand command = newSqlCommand(sql, connection)) {

connection.Open();

using(SqlDataReader reader = command.ExecuteReader()) {

ddlSalesPeople.DataSource = reader;

ddlSalesPeople.DataBind();

}

}

Controls.Add(ddlSalesPeople);

base.CreateChildControls();

ChildControlsCreated = true;

}

// Save settings to web part

publicoverridebool ApplyChanges()

{

EnsureChildControls();

RecentSales webPart = (RecentSales) this.WebPartToEdit;

webPart.SalesPersonId =

Int32.Parse(ddlSalesPeople.SelectedValue);

returntrue;

}

// Retrieve settings from web part

publicoverridevoid SyncChanges()

{

EnsureChildControls();

RecentSales webPart = (RecentSales) this.WebPartToEdit;

ddlSalesPeople.SelectedValue = webPart.SalesPersonId.ToString();

}

A Couple of notes about my new editor:

Once again I’ve hardcoded my connection string and SQL query. I’m ok with that here. 🙂

CreateChildControls() is relatively straight forward, adding in the label for display purposes, and then the drop down list.

ApplyChanges() and SyncChanges() work just as prescribed, reading the WebPartToEdit property to access the web part.

After creating the EditorPart, the last step is to configure the web part to use the new editor. This is done by performing the following actions:

1. Update the web part to implement IWebEditable. IWebEditable has two methods:

CreateEditorParts() – used to create an instance of the desired editor web part

WebBrowsableObject() – used to return the current instance of the web part for access in the editor

For our example an implementation would look like this:

EditorPartCollectionIWebEditable.CreateEditorParts()

{

List<EditorPart> editors = newList<EditorPart>();

RecentSalesEditor editor = newRecentSalesEditor();

editor.ID = this.ID + "EditorPart";

editors.Add(editor);

returnnewEditorPartCollection(editors);

}

objectIWebEditable.WebBrowsableObject

{

get { returnthis; }

}

2. Remove every attribute from the property except Personalizable. Or more specifically, make sure that WebBrowsable is gone. While having the other ones left behind won’t hurt anything, they’re superfluous since we won’t be counting on SharePoint to create the editor for us. The resultant property looks like this:

[Personalizable(PersonalizationScope.User)]

publicInt32 SalesPersonId { get; set; }

The end result in the editor will look a little like this:

Creating usable web parts will go a long way to ensuring reuse and adoption of your SharePoint implementation. With that, here’s today’s My 5.

My 5 components to a good web part

Configurable – A good web part should be configurable by the user. Each user will have different needs for certain web parts, and by allowing users the ability to configure them they’ll become more valuable.

Self-documenting – Users may or may not actually read any documentation that’s separate from the web part. Make sure your properties are clearly named, and provide any supporting documentation on the editor.

Connectable – When appropriate, take advantage of the ability to connect web parts. This allows your users to create parent/child views with ease.

Validates User Input – Either implicitly through controls like drop down lists, or explicitly by using validation controls, make sure you validate every value to ensure it is acceptable.

Provide Custom Error Messages – Unhandled exceptions in a web part will crash the entire page. Make sure you catch all exceptions and display friendly error messages.

[1] In my example, I’m using a custom web part. I did this merely for simplicity of the blog post. I generally favour visual web parts, but adding properties to a visual web part requires a couple of additional steps. The steps I walked through above can be implemented with a visual web part using the same techniques, remembering that the property needs to be added to the web part class, and the user control will be responsible for reading and using the value. If you’re interested, I wrote a blog post on adding properties to visual web parts.