Quantcast
Channel: Microsoft Dynamics 365 Community
Viewing all articles
Browse latest Browse all 13977

Microsoft Dynamics CRM 2013 HTML5 Task board

$
0
0
 I have recently been working on some internal tools that we use at TSG, and thought I would share some of experience with this. The out of the box Task board in TFS 2013 doesn’t allow for it to be used with multiple projects, so I was tasked with creating a drag and drop Task board that would work with multiple projects, all of their backlog items, and the tasks associated with them. As I was creating this, I thought this could also be used with CRM 2013 as a customer services tool to display tasks per case or user, team or customer.
 
The basic layout I needed to replicate was similar to the TFS task board, which is a basic post it note style board, where you move tasks between different stages of a product backlog item or case’s lifecycle.
 
OK, so where do we start?
 
Well it depends really on who needs access, in our example, let’s create an ASP.NET MVC 5 web application. This means we can allow either a public view, or an intranet view of the data we want to publish.
After we have create an ASP.NET MVC application, we need to add a class that will serve as our data model. So we create a POCO class as below within our Models folder:
 
publicclassWorkItemsModel
{
    publicList<WorkItemModel> Items { get; set; }
}
 
publicclassWorkItemModel
{
    publicint Id { get; set; }
    publicstring Project { get; set; }
    publicstring WebEditorUrl { get; set; }
    publicstring AssignedTo { get; set; }
    publicstring Title { get; set; }
    publicdouble RemainingWork { get; set; }
    publicstring LifecycleStage { get; set; }
    publicint ParentId { get; set; }
    publicList<WorkItemModel> Tasks { get; set; }
}
 
The key thing to note with this data model class are the names of each of the properties as we will use these when we use data binding in Knockout JS later on.
 
Next we need to modify the default controller that Visual Studio creates for us. Here in our Main Index action we call our GetWorkItems method from the relevant helper class, serialise the class to a JSON object, we push a string array of each of the Stages we want into the ViewBag, grab the current Name of the user we are executing as mainly to check that we are impersonating / passing the correct credentials across to CRM, and finally return the view.
 
publicclassHomeController : Controller
{
    publicActionResult Index()
    {
        var workItems = CrmHelper.GetWorkItems();
           
        ViewBag.SerializedData = JsonConvert.SerializeObject(workItems, Formatting.None);
        ViewBag.LifecycleStages = CrmHelper.GetLifecycleStages();
 
        ViewBag.CurrentUser = HttpContext.User.Identity.Name;
 
        return View(workItems);
    }
 
    [HttpPost]
    publicActionResult UpdateWorkItem(string id, string lifecycleStage)
    {
        CrmHelper.UpdateWorkItem(int.Parse(id), lifecycleStage);
 
        returnnewContentResult { Content = "OK " + id + " " + lifecycleStage }; 
    }
}
 
We also create a post back action to update a attribute in CRM when we move the item between the various different stages.
 
Note: We could use Web API 2 here, but as this is a simple example, there is no real need, it just increases the complexity of the application for what is just a one line method call.
 
Now to the Cloud! I mean HTML, JavaScript, and CSS…
 
As part of the MVC application we get the jQuery and Knockout JS libraries included, we don’t however get jQuery UI. We need jQuery UI to be able to drag our post it notes around on the page, so we can simply download that and add it to our project.
 
In our Index razor .cshtml template we add some JavaScript, we could put this in its own .js file which under a normal project conditions you would do for encapsulated separation.
 
<script>       
    var shouldCancel = false;
    var _dragged;
    ko.bindingHandlers.drag = {
        init: function (element, valueAccessor, allBindingsAccessor, viewModel) {
            var dragElement = $(element);
            var dragOptions = {
                helper: 'clone',
                opacity: 0.50,
                zIndex: 100,
                revert: function () {
                    if (shouldCancel) {
                        shouldCancel = false;
                        returntrue;
                    } else {
                        returnfalse;
                    }
                },
                start: function () {
                    _dragged = ko.utils.unwrapObservable(valueAccessor().value);
                },
                cursor: 'default'
            };
            dragElement.draggable(dragOptions).disableSelection();
        }
    };
 
    ko.bindingHandlers.drop = {
        init: function (element, valueAccessor, allBindingsAccessor, viewModel) {
            var dropElement = $(element);
            var dropOptions = {
                hoverClass: "dragHover",
                drop: function (event, ui) {                       
                    var dropped = ui.draggable;
                    var parentRowId = $(this).parent("tr").attr("id");
                    if (parentRowId == _dragged.ParentId) {
 
                        $(dropped).detach().appendTo($(this).find(".board-tiles"));
                        $(dropped).find('.witError').hide();
                        $(dropped).find('.witInfo').hide();
                        $(dropped).find('.witSaving').show();
                           
                        _dragged.LifecycleStage = $(this).attr("id");
                        $.ajax({
                            type: 'POST',
                            url: '/Home/UpdateWorkItem',
                            cache: false,
                            contentType: 'application/json; charset=utf-8',
                            data: JSON.stringify({ Id: _dragged.Id, lifecycleStage: _dragged.LifecycleStage }),
                            success: function (data) {
                                $(dropped).find('.witInfo').show();
                                $(dropped).find('.witSaving').hide();
                            },
                            error: function (req, status, err) {
                                $(dropped).find('.witError').show();
                                $(dropped).find('.witSaving').hide();
                            }
                        });
                    } else {
                        shouldCancel = true;
                    }
                }
            };
            dropElement.droppable(dropOptions);
        }
    };
 
    var TaskBoardViewModel = function (data) {
        this.Items = data.Items;
        this.taskStack = ko.observableArray();
        this.lastTaskUpdated = ko.computed({
            read: function () { returnthis.taskStack().length ? this.taskStack()[0] : ""; },
            write: function (value) { this.taskStack.unshift(value); },
            owner: this
        });
    };
 
    var vm = new TaskBoardViewModel(@Html.Raw(ViewBag.SerializedData));       
    ko.applyBindings(vm);       
</script>   
 
Here we have a couple of variables to hold the item being dragged, and whether we need to cancel the drop. We then declare two functions to handle the drag and drop event binding from Knockout JS. The key thing to note here is that draggable and droppable prototypes are from the jQuery UI library.
 
In drag we simply handle whether we need to revert / cancel the drag and assign the currently selected View Model item to the _dragged variable.
 
In drop we check whether the item has been moved to a different table row, and cancel if so, we then detach the item being dragged from its current parent div collection of items and attach it to the new parent’s div collection of items.
 
The final thing we do as part of the drop is to show the saving message div, update our View Model item to the correct Stage, and make an ajax post to the Home controller to update the status in CRM. If the post fails, i.e. something went wrong on the server, we handle this via an error call back and display an appropriate message.
 
We then need to define our View Model, so we create an object to hold the data items we want to display, an Observable Array of any items we have dragged / dropped, and the last item that was moved. The last item that was moved is assigned to as part of the Knockout JS drop data bind.
 
The final thing we can do as part of the script is to create a View Model based on the JSON serialised data that we generated in our MVC Controller, and get Knockout JS to apply the bindings and format the HTML correctly.
 
So that’s the JS beauty, what about the HTML brains? (I’m sure that’s the wrong way around…)
 
Well the HTML is built up of a table, with a number of rows and columns. We can put a title at the top, and table headers for each of the stages we can move a post it note to.
 
We can then data bind the table body using Knockout JS to our data using the foreach data bind keyword. This means that Knockout JS will render a table row for each item in our data, displaying the Title and Remaining Work involved.
 
We then create table data mark-up for each of the stages, and use data binding in Knockout JS to cause the drop event to update the lastTaskUpdate property of our View Model.
 
<tablecellpadding="0"cellspacing="0">
    <trclass='taskboard-row taskboard-row-summary'id='taskboard-summary-row-0'>
        <tdclass='heading'colspan="5">
            Team Task Board
        </td>
        <tdclass='heading'>
            @ViewBag.CurrentUser
        </td>
    </tr>
    <trclass='taskboard-row taskboard-row-summary'id='taskboard-summary-row-0'>
        <thclass='taskboard-stage'></th>
           
    @foreach (var item in ViewBag.LifecycleStages)
    {
        <thclass='taskboard-stage'>@item</th>
    }
                                   
    </tr>
    <tbodydata-bind="foreach: Items">
        <trclass='taskboard-row taskboard-row-summary'data-bind="attr: {'id': Id}">
            <tdclass='taskboard-parent'>
                <divdata-bind="text: Title"></div>
                <divdata-bind="text: RemainingWork"></div>
            </td>
            @foreach (var item in ViewBag.LifecycleStages)
            {
            <tdclass='taskboard-cell'id="@item"data-bind="drop: {value: $parent.lastTaskUpdated}">
                <divclass="board-tiles"data-bind="foreach: Tasks">
                    <!-- ko if: LifecycleStage == '@item' -->
                    <divclass="board-tile-draggable"data-bind="drag: {value: $data}">
                        <divclass="board-tile">
                            <divclass='witTitle ellipsis'data-bind="text: Title"></div>
                            <divclass='witInfo'>
                                <divclass='witRemainingWork ellipsis'data-bind="text: RemainingWork"></div>
                                <divclass='witAssignedTo ellipsis'data-bind="text: AssignedTo"></div>
                            </div>
                            <divclass="witSaving"hidden="hidden">Saving</div>
                            <divclass="witError"hidden="hidden">Error while Saving</div>
                        </div>
                    </div>
                    <!-- /ko -->
                </div>
            </td>
            }
        </tr>
    </tbody>       
</table>
 
Now that we have our rows of items, we need to display our tasks. Again we can use the foreach keyword in Knockout JS to create a number of div elements, each of which can contain elements which display the data from our task via data binding. We use the Knockout JS “ko if” mark-up syntax to control the flow of what elements from our task list are displayed in each of the different stages.
 
The key thing to note in the HTML is that the property names we use in our data-bind are the same as the property names in the POCO Model.
 
On the drag data-bind event we pass in the $data object, which is just the item from we are working with within the foreach section of our Knockout JS mark-up.
 
Finally the CSS, I’m not going to explain this cause it’s really just a bunch of styles…
 
.heading
{
    font-family : SegoeUILight, SegoeUI, Tahoma, Arial, Verdana;   
    font-size: 16px;
    color : #000000;   
    height: 100px;
}
 
.board-tiles {
         
    position: relative;
    display: inline-block;
    margin: 0px;
}
 
.board-tile-draggable {
         
    position: relative;
    display: inline-block;
    margin: 0px;
}
 
.board-tile
{        
    font-size: 9pt;
    font-family : SegoeUILight, SegoeUI, Tahoma, Arial, Verdana;
    position: relative;
    display: inline-block;
    vertical-align: top;
    width: 120px;
    height: 78px;
    margin: 5px;
    border-left: 6pxsolid#F2CB1D;
    padding: 4px4px4px4px;
    background-color: #F6F5D2;
    border-top-right-radius: 15px;  
    cursor: pointer;
}
 
.witTitle
{
    margin-right: 6px;
    height: 63px;
    color: #000000;
}
 
.witAssignedTo
{
    white-space: nowrap;
    float: right;     
    min-width: 10px;
    color: #595959;
}
 
.witRemainingWork
{
    font-weight: bold;
    white-space: nowrap;
    float: left;  
    color: #000000;
}
 
.witSaving
{
    font-weight: bold;
    color: #FFFFFF;
    background-color: #4D6082;
    display: none;
    text-align: center;
}
 
.witError
{
    font-weight: bold;
    color: #FFFFFF;
    background-color: #FF0000;   
    display: none;
    text-align: center;
}
 
.ellipsis
{
    overflow: hidden;
    text-overflow: ellipsis;
    -o-text-overflow: ellipsis
    -ms-text-overflow: ellipsis;
}
 
.taskboard-parent
{
    font-weight: normal;
    width: 200px;   
    vertical-align: top;
    padding: 4px004px;
    border-width: 1px;
    border-bottom-style: solid;
    border-left-style: solid;
    border-color: #D9D9D9;  
}
 
.taskboard-cell
{
    width: 400px;
    vertical-align: top;
    padding: 4px004px;
    border-width: 1px;
    border-bottom-style: solid;
    border-left-style: solid;
    border-color: #D9D9D9;
}
 
.taskboard-parent,.taskboard-cell
{
    font-family : SegoeUILight, SegoeUI, Tahoma, Arial, Verdana;   
    font-size: 16px;
    color : #000000;
    text-overflow: ellipsis;
    text-align: left;   
}
 
.taskboard-stage
{
    font-family : SegoeUILight, SegoeUI, Tahoma, Arial, Verdana;   
    font-size: 14px;
    color : #000000;
    text-overflow: ellipsis;
    text-align: left;  
    text-transform: uppercase;
    font-weight: normal;
    width: 200px;
    vertical-align: top;
    padding: 4px004px;
    border-width: 1px;
    border-bottom-style: solid;
    border-left-style: solid;
    border-color: #D9D9D9;  
}
 
.dragHover
{
    background-color: #EBF1DE;  
}
 
 
So that just about wraps up this example of a HTML 5 task board using jQuery, Knockout JS and ASP.NET MVC.
 
Hopefully you will find this useful and can see the practical use with Microsoft Dynamics CRM 2013…
 
@simonjen1

Viewing all articles
Browse latest Browse all 13977

Trending Articles