Improving the SharePoint User Experience with Custom Web Part Editors

ChrisWeb03Today’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 Smile. 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)]

public Int32 SalesPersonId { get; set; }

 

private GridView gvRecentOrders;

private String connectionString = //AdventureWorks connection string

private String sql = @"SELECT TOP 10 OrderDate

                         , TotalDue

                    FROM Sales.SalesOrderHeader

                    WHERE SalesPersonID = @SalesPersonID

                    ORDER BY OrderDate DESC";

             

protected override void OnPreRender(EventArgs e)

{

    if(SalesPersonId == 0) {

        DisplayEditLink();

    } else {

        DisplayRecentOrders();

    }

}

 

private void DisplayEditLink()

{

    HyperLink editLink = new HyperLink();

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);

}

 

private void DisplayRecentOrders()

{

    gvRecentOrders = new GridView();

    using (SqlConnection connection =

               new SqlConnection(connectionString))

    using (SqlCommand command = new SqlCommand(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 the GridView.
  • 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:

private String connectionString = //AdventureWorks connection string

private String 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";

 

private DropDownList ddlSalesPeople;

private Label lblSalesPeople;

 

protected override void CreateChildControls()

{

    this.Title = "Recent Sales Properties";

 

    lblSalesPeople = new Label();

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

    Controls.Add(lblSalesPeople);

 

    ddlSalesPeople = new DropDownList();

    ddlSalesPeople.DataValueField = "SalesPersonId";

    ddlSalesPeople.DataTextField = "Name";

 

    using(SqlConnection connection =

               new SqlConnection(connectionString))

    using(SqlCommand command = new SqlCommand(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

public override bool ApplyChanges()

{

    EnsureChildControls();

    RecentSales webPart = (RecentSales) this.WebPartToEdit;

    webPart.SalesPersonId =

        Int32.Parse(ddlSalesPeople.SelectedValue);

    return true;

}

 

// Retrieve settings from web part

public override void 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:

 

EditorPartCollection IWebEditable.CreateEditorParts()

{

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

    RecentSalesEditor editor = new RecentSalesEditor();

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

    editors.Add(editor);

    return new EditorPartCollection(editors);

}

 

object IWebEditable.WebBrowsableObject

{

    get { return this; }

}

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)]

public Int32 SalesPersonId { get; set; }

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

RecentSalesEditor

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

  1. 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.
  2. 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.
  3. Connectable – When appropriate, take advantage of the ability to connect web parts. This allows your users to create parent/child views with ease.
  4. 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.
  5. 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.

Oh – and before I forget. You can download the source code!

[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.