ASP.NET MVC: Enhancing The WebGrid - Inline Editing using SignalR

June 8th, 2015

One of the major features of a grid is the ability to edit data inline. Today, I provide a way to edit and save your data from a WebGrid using SignalR.

At the beginning of the WebGrid series, I mentioned I wanted to push the limits and see what we could do with a generic WebGrid and make it fully loaded with Grid features

One feature that I neglected to add was a way to edit data in the grid itself. With that said, let's add another grid feature!

Since we are avoiding post backs to streamline the user experience, we'll be using some heavy jQuery with SignalR for this example. We'll also leverage our past project of the SignalR WebGrid we built to enhance our WebGrid even more.

Let's start with the HTML first.

Displaying Columns Differently

We need the ability to display the data and allow the data to be edited.

Without using a ton of JavaScript to dynamically create editable fields, we need a way to display a value and an editable field to make this work.

For our WebGrid, let's change how the columns are displayed by adding an HtmlHelper to each column.

    @MvcHtmlString.Create(
        grid.GetHtml(
            htmlAttributes: new
            {
                id = "grid",
                @class = "table table-bordered table-striped table-condensed"
            },
            emptyRowCellValue: "No Records Found",
            headerStyle: "grid-header",
            columns: grid.Columns(
                grid.Column(header: "{CheckBoxHeading}",
                    format: @<text><input id="select" class="box" name="select" type="checkbox" value="@item.Value.Id"/></text>,
                    style: "text-center checkbox-width"),
                grid.Column("UserName" , format: item => Html.EditableTextBox((item.Value as User).UserName, (item.Value as User), "UserName")),
                grid.Column("FirstName", format: item => Html.EditableTextBox((item.Value as User).FirstName, item.Value as User, "FirstName")),
                grid.Column("LastName" , format: item => Html.EditableTextBox((item.Value as User).LastName, item.Value as User, "LastName")),
                grid.Column("LastLogin", format: item => Html.EditableDateTime((item.Value as User).LastLogin, item.Value as User, "LastLogin"))
                )
            )
            .ToString()
            .Replace("{CheckBoxHeading}""<div class='text-center'><input type='checkbox' id='allBox'/></div>")
        )
}

Our Html.EditableTextBox requires a value, the object, and a name for our control. The HtmlHelpers are Html.EditableTextBox() and Html.EditableDateTime().

Helpers\Html\EditableHelpers.cs

public static class EditableHelpers
{
    public static MvcHtmlString EditableTextBox(this HtmlHelper helper,
        string value, User user, string name)
    {
        // Text Display
        var span = new TagBuilder("span") {InnerHtml = value};
        span.AddCssClass("cell-value");
 
        // Input display.
        var formatName = HtmlHelper.GenerateIdFromName(name);
        var uniqueId = String.Format("{0}_{1}", formatName, user.Id);
        var input = helper.TextBox(uniqueId,
            value, new {@class = "hide input-sm"});
 
        var result = String.Concat(
            span.ToString(TagRenderMode.Normal),
            input.ToHtmlString()
            );
 
        return MvcHtmlString.Create(result);
    }
 
    public static MvcHtmlString EditableDateTime(this HtmlHelper helper,
        DateTime value, User user, string name)
    {
        // Text Display
        var span = new TagBuilder("span") {InnerHtml = value.ToString("yyyy-MM-dd")};
        span.AddCssClass("cell-value");
 
        // Input display.
        var formatName = HtmlHelper.GenerateIdFromName(name);
        var uniqueId = String.Format("{0}_{1}", formatName, user.Id);
        var input = helper.TextBox(uniqueId, value.ToString("yyyy-MM-dd"),
            new {@type = "date", @class = "hide input-sm"});
 
        var result = String.Concat(
            span.ToString(TagRenderMode.Normal),
            input.ToHtmlString()
            );
 
        return MvcHtmlString.Create(result);
    }
}

What we've accomplished is created a span tag with a value that is visible in the WebGrid and a editable field to the right of it that is hidden.

When we want to enable the editing of a row, we hide the span's "cell-value" and display the editable fields for the row. If we hit a cancel button, we hide the editable row and display and update the cell value.

One thing you may notice is the formatting of the date/time in the span creation and the TextBox's ToString() formatting of the DateTime field.

The TextBox created is not your average edit box. The edit box is of type 'date' from the HTML 5. This enhancement provides you two features:

  1. When specified as type 'date,' you get an automatic dropdown calendar attached to the edit box for entering dates.
  2. When using dates, the value requires it to be in a specific format (yyyy-MM-dd) for the value to be editable in the Date TextBox.

Of course, your browser has to support the HTML 5 standard for this to work.

Toolbar: Row Actions

How are we going to edit the row? Do we click in the row? Do we double-click? Or click a toolbar button.

I felt that the best way to edit a row is to put it into edit mode by using a toolbar button.

So we do need another column for our action buttons.

Add the bolded line at the end to your HTML Web Grid.

.
.
columns: grid.Columns(     grid.Column(header: "{CheckBoxHeading}",         format: @<text><input id="select" class="box" name="select" type="checkbox" value="@item.Value.Id"/></text>,         style: "text-center checkbox-width"),     grid.Column("UserName" ,          format: item => Html.EditableTextBox((item.Value as User).UserName, (item.Value as User), "UserName")),     grid.Column("FirstName",          format: item => Html.EditableTextBox((item.Value as User).FirstName, item.Value as User, "FirstName")),     grid.Column("LastName" ,          format: item => Html.EditableTextBox((item.Value as User).LastName, item.Value as User, "LastName")),     grid.Column("LastLogin",          format: item => Html.EditableDateTime((item.Value as User).LastLogin, item.Value as User, "LastLogin")),     grid.Column("Options",          format: item => Html.DisplayRecordOptions(item.Value as User), canSort: false)          ) )
.
.

Now that we have our column with options, we need another HtmlHelper for displaying our actions for the row. We'll get into the JavaScript a little later.

Here is the Html.DisplayRecordOptions with accompanying methods to build our toolbar (add these to your EditableHelpers.cs file).

public static MvcHtmlString DisplayRecordOptions(this HtmlHelper helper, User user)
{
    // Text Display
    var toolbar = new TagBuilder("ul");
    toolbar.AddCssClass("record-toolbar");
 
    var result = String.Concat(
        GetEditButton().ToString(TagRenderMode.Normal),
        GetSaveButton().ToString(TagRenderMode.Normal),
        GetCancelButton().ToString(TagRenderMode.Normal)        
        );
 
    toolbar.InnerHtml = result;
 
    return MvcHtmlString.Create(toolbar.ToString(TagRenderMode.Normal));
}
 
private static TagBuilder GetEditButton()
{
    var editButton = new TagBuilder("li")
    {
        InnerHtml = GetIcon("edit")
    };
    editButton.AddCssClass("edit-button btn btn-default btn-sm");
    editButton.Attributes.Add("title""Edit Record");
    return editButton;
}
 
private static TagBuilder GetCancelButton()
{
    var cancelButton = new TagBuilder("li")
    {
        InnerHtml = GetIcon("ban-circle")
    };
    cancelButton.AddCssClass("cancel-button hide btn btn-default btn-sm");
    cancelButton.Attributes.Add("title","Cancel Editing");
    return cancelButton;
}
 
private static TagBuilder GetSaveButton()
{
    var saveButton = new TagBuilder("li")
    {
        InnerHtml = GetIcon("floppy-disk")
    };
    saveButton.AddCssClass("save-button hide btn btn-default btn-sm");
    saveButton.Attributes.Add("title""Save Record");
    return saveButton;
}
 
private static string GetIcon(string iconName)
{
    var icon = new TagBuilder("i");
    icon.AddCssClass(String.Format("glyphicon glyphicon-{0}", iconName));
    return icon.ToString(TagRenderMode.Normal);
}

The GetIcon() method uses Bootstrap's icons. Pass the icon name into the method and you receive an "I"-tagged icon for your toolbar button.

Make it Pretty!

Along with the row toolbar, I realize we need some CSS to make the buttons look a little decent.

Add these to the top of your Index.cshtml.

.record-toolbar { padding0 10pxmargin-bottom0}
#grid td:nth-child(6) {     width100px }

Prepare your JavaScript

From here on out, we'll be writing jQuery for the remainder of this WebGrid feature, adding onto the index.cshtml.

First, we need to display the data in the grid. This is automatically handled when we initially display the WebGrid. The editable fields in each cell are automatically hidden when first rendered.

We need a way to toggle the data from the editable fields.

The initial rendering of the row contains an Edit button. The Edit button will toggle these controls (and buttons) on and off.

$(".edit-button").on("click"function () {
    var row = $(this).closest("tr");
    $(".record-toolbar li", row).toggleClass("hide");
    $("input, select, textarea", row).toggleClass("hide");
    $(".cell-value", row).toggleClass("hide");
});

Since we have our toolbar in a unordered list (UL), we can use the toggleClass method to hide and show the right buttons when we're in edit mode.

We also need to show the editable fields and hide the cell values, hence the last two lines.

But when we have our edit mode, our save and cancel button are enabled and visible.

We need our click event for our cancel and save button.

Cancel the Editing

The cancel and save buttons are easy. Just reverse the process.

$(".cancel-button").on("click"function () {
    var row = $(this).closest("tr");
    $(".record-toolbar li", row).toggleClass("hide");
    $(".cell-value", row).toggleClass("hide");
    $("input, select, textarea", row).toggleClass("hide");
});
 
$(".save-button").on("click"function () {
    var row = $(this).closest("tr");
    $(".record-toolbar li", row).toggleClass("hide");
    $("input, select, textarea", row).toggleClass("hide");
    $(".cell-value", row).toggleClass("hide");
});

Regarding the duplicated code, bear with me, I'll explain in a bit.

Now we have the ability to display our data and edit the fields.

However, we do have a couple of outstanding items.

  1. When entering edit mode, we need the cell value copied into each editable field.
  2. When we leave edit mode, we need to perform the reverse process and copy the field data into the cell value.
  3. When saving, we need to send it back to the server.

Here is the updated EditButton click and SaveButton click.

$(".edit-button").on("click"function () {
    var row = $(this).closest("tr");
    $(".record-toolbar li", row).toggleClass("hide");
    $("input, select, textarea", row).toggleClass("hide");
    $("td", row).each(function () {
        var cell = $(this, row);
        var cellValue = $(".cell-value", cell);
        if (cellValue.length > 0) {
            $("input", cell).val(cellValue.text());
        }
    });
    $(".cell-value", row).toggleClass("hide");
});
$(".save-button").on("click"function () {
    var row = $(this).closest("tr");
 
    $("td", row).each(function () {
        var cell = $(this, row);
        var inputValue = $("input", cell);
        $(".cell-value", cell).text(inputValue.val());
    });
 
    $(".record-toolbar li", row).toggleClass("hide");
    $("input, select, textarea", row).toggleClass("hide");
    $(".cell-value", row).toggleClass("hide");
});

On the edit button code, I had to make both the span.cell-value and the editable fields visible before I started hiding the cell values. If I hid the cell values first, I wouldn't be able to get a reference because they were "hidden." 

The Cool Kid On The Block: SignalR

I have been following SignalR for the last 3 years and I consider it to be a game-changer in the web world. Incorporating SignalR always brings a smile to my face.

Which brings me to our last task. We need to save our record to the database using SignalR.

Let's add a JavaScript object to collect and create our data based on a row so we can send it back to the server.

function getRecord(row) {
    return {
        id: row.find(':nth-child(1)').find("input:checkbox").val(),
        checkBoxCell: row.find(':nth-child(1)').html(),
        userName: row.find(':nth-child(2)').find("input").val(),
        firstName: row.find(':nth-child(3)').find("input").val(),
        lastName: row.find(':nth-child(4)').find("input").val(),
        lastLogin: row.find(':nth-child(5)').find("input").val()
    };
};

This JavaScript function (placed after the FormatDate function) takes a row parameter and pulls the data from the editable fields in the row and creates a brand new JavaScript object to pass back to the server.

Notice that all of the properties are camelCase AND they match the User class members exactly (aside from the checkBoxCell property).

When this object is passed back to the server using SignalR, it will map the JavaScript properties to a User class on the server and pass it in as a parameter. No mapping necessary.

Now, to the C# server side.

Open your WebGridHub.cs and add this method to your hub:

public Task SaveRecord(User user)
{
    var record = _repository.GetById(user.Id);
    record.UserName = user.UserName;
    record.FirstName = user.FirstName;
    record.LastName = user.LastName;
    record.LastLogin = user.LastLogin;
    _repository.SaveChanges();
 
    return Clients.Caller.recordSaved();
}

Based on the user object passed into this method, we load the user by Id from the table, assign the new values to the object, and finally save the changes to the table.

After everything is done, we call the client-side JavaScript method called recordSaved().

Back to the client side!

Finishing Touches

On the save button, we need to get the row's data, pass that record back, and save it to the database.

Here are the final touches of the Save Button JavaScript click event (in bold):

$(".save-button").on("click"function () {
    var row = $(this).closest("tr");
 
    var record = getRecord(row);
    webGridHubClient.server.saveRecord(record);
 
    $("td", row).each(function () {
        var cell = $(this, row);
        var inputValue = $("input", cell);
        $(".cell-value", cell).text(inputValue.val());
    });
 
    $(".record-toolbar li", row).toggleClass("hide");
    $("input, select, textarea", row).toggleClass("hide");
    $(".cell-value", row).toggleClass("hide");
});

Notice the saveRecord is camelCase where the C# method signature is Pascal Case. Always remember that.

I hope you didn't forget that we need our recordSaved method on the client-side.

webGridHubClient.client.recordSaved = function () {
    alert("Record Saved.");
};

As you can see, I kind of chickened-out and used a standard alert box. I leave this exercise to the reader to make a better UI for displaying a proper notification event to their users.

Conclusion

This WebGrid is coming together quite nicely.

We have exporting capabilities, inline-editing capabilities, a paging data feature, and batch processing records.

If anyone has additional features they want to add to this WebGrid, please let me know and I will continue enhancing this awesome WebGrid!

Series: Enhancing the WebGrid