Saturday, November 4, 2017

An AngularJS Dashboard, Part 5: Tile Configuration UI and Dashboard Persistence

NOTE: for best results, view the http: version of this page (else you won't get syntax highlighting).

This is Part 5 in a series on creating a dashboard in AngularJS. I'm blogging as I progressively create the dashboard, refining my Angular experience along the way.

Previously in Part 4, we added new chart tiles and completed enough of the back end that dashboard layout and data are queried from a database. However, we haven't yet done any work to allow the user to customize their dashboard and persist those changes for the future.

Today, we're going to:

  • Add a user interface for configuring tiles.
  • Add persistence of dashboard changes for the user.
  • Add tile actions to edit/add/remove tiles, and to reset the dashboard.

Here's what we'll be ending up with:

Dashboard with Configuration Dialog Open

Tile Configuration UI

For adding new tiles or configuring existing ones, the dialog shown below is used. It appears docked at right when the Configure Tile or Add New Tile actions are selected from a tile's menu.

Tile Configuration UI

When I first started implementing the UI, I approached it jQuery-style, where I used explicit code to set the input control values based on properties of the selected tile. Likewise, when the UI was saved similar code would copy input control values back into tile properties. Although this worked, I was dissatisfied with it. For one thing, changes to tiles were only visible when you saved your changes, not as you interacted with the form.

I realized I wasn't doing this the Angular way. I re-implemented using Angular directives, and I now have a form that is automatically linked to the tile being configured. As changes are made in the dialog, they immediately show visually which is much nicer for the user. I also eliminated a lot of code.

Just how is this linkage accomplished?

  • The visibility of the configuration dialog is controlled by the ng-style directive on line 4: when the controller's configuring variable is true, the dialog is visible; and when false, the dialog is hidden. 
  • The ng-model directive is used on many of the input controls (such as lines 7, 11, and 15). It sets a 2-way binding between a tile property and an input control: when one changes, the other is updated. 
  • There are a number of labels and input controls that only apply to certain tile types; ng-if is used to control their visibility (lines 48, 51, 52).
<!-- Configure Tile Panel -->

<div id="configTilePanel" style="position: fixed; width: 400px; right: 0; top: 0; background-color: #FFFFFF; border: 1px solid black"
        ng-style="{'visibility': $ctrl.configuring?'visible':'hidden'}">
    <div style="font-size: 18px !important; margin-bottom: 8px; background-color: navy; color: white"> {{$ctrl.configureTileTitle}}<div style="float: right;" onclick="cancelConfigureTile();">X </div></div>
    <table style="width: 100%">
        <tr><td>Title:  </td><td><input id="configTileTitle" style="width: 100%" ng-model="$ctrl.tiles[$ctrl.configIndex].title"></td></tr>
        <tr>
            <td>Type:  </td>
            <td>
                <select id="configTileType" style="width: 100%" ng-model="$ctrl.tiles[$ctrl.configIndex].type" ng-change="$ctrl.TileTypeChanged($ctrl.configIndex);">
                    <option value="counter">counter</option>
                    <option value="bar">bar chart</option>
                    <option value="column">column chart</option>
                    <option value="donut">donut chart</option>
                    <option value="kpi">kpi</option>
                    <option value="pie">pie chart</option>
                    <option value="table">table</option>
                </select>
            </td>
        </tr>
        <tr>
            <td>Color:  </td>
            <td>
                <input id="configTileColor" type="color" class="configTileColor" style="width: 100%" ng-model="$ctrl.tiles[$ctrl.configIndex].color" />
            </td>
        </tr>
        <tr>
            <td>Width:  </td>
            <td>
                <select id="configTileWidth" ng-model="$ctrl.tiles[$ctrl.configIndex].width">
                    <option value="1">1</option>
                    <option value="2">2</option>
                </select>
            </td>
        </tr>
        <tr>
            <td>Height:  </td>
            <td>
                <select id="configTileHeight" ng-model="$ctrl.tiles[$ctrl.configIndex].height">
                    <option value="1">1</option>
                    <option value="2">2</option>
                </select>
            </td>
        </tr>
        <tr><td>Data Source:  </td><td>
            <select id="configTileDataSource" ng-model="$ctrl.tiles[$ctrl.configIndex].dataSource" ng-change="$ctrl.UpdateTileData($ctrl.configIndex);">
                <option ng-repeat="query in $ctrl.queries" value="{{query.QueryName}}" ng-if="query.ValueType==$ctrl.dataSourceType()">{{query.QueryName}}</option>
            </select>
        </td></tr>
        <tr ng-if="$ctrl.tiles[$ctrl.configIndex].type=='counter' || $ctrl.tiles[$ctrl.configIndex].type=='kpi'"><td>Label:  </td><td><input id="configTileLabel" style="width: 100%" ng-model="$ctrl.tiles[$ctrl.configIndex].label" /></td></tr>
        <tr ng-if="$ctrl.tiles[$ctrl.configIndex].type=='counter' && $ctrl.tiles[$ctrl.configIndex].dataSource=='inline'"><td>Value:  </td><td><input id="configTileValue" style="width: 100%" /></td></tr>
        <tr><td>Link:  </td><td><input id="configTileLink" style="width: 100%" ng-model="$ctrl.tiles[$ctrl.configIndex].link" /></td></tr>
        <tr><td colspan="2" style="text-align: right">
            <button id="cancelButton" onclick="cancelConfigureTile();" class="btn" style="background-color: black; color: white">Cancel</button>  
            <button id="saveButton" onclick="saveConfigureTile(true);" class="btn" style="background-color: green; color: white">Save</button>
        </td></tr>
    </table>
</div>
Markup for Tile Configuration Dialog

Color Selection

For color selection, the dialog makes use of the Spectrum Color Picker, which does a nice job of letting users select from the palette of colors we've chosen to make available. Speaking of which, we've expanded our available tile colors.

Color Picker with an Expanded Palette

Many of the colors in the tile palette are the same as offered by the TFS dashboard, but I've also made a few changes. Here's a sampling of how some of these colors look on tiles.

Dashboard with some new Tile Colors

One other thing to note about tile colors. Previously, when we had a smaller color palette, we used classes to set tile colors--which allowed us to also set the foreground color to white or black, whichever gave the best contrast for the tile color. We've now made that automatic, with the addition of  a new controller function, tileTextColor(id), which uses an algorithm found on the Internet for determining the best contrast color for a background color.
// Return the best text color for a tile. Pass id ('1', '2', ...).

self.tileTextColor = function (id) {
    return self.textColor(self.tiles[parseInt(id) - 1].color);
}

// textColor('#rrggbb') : Given a background color (rrggbb or #rrggbb). Return '#000000' or '#FFFFFF'.
// Courtesty of https://24ways.org/2010/calculating-color-contrast/

self.textColor = function(hexcolor) {
    var color = hexcolor;
    if (color.length === 7) color = color.substr(1);
    var r = parseInt(color.substr(0, 2), 16);
    var g = parseInt(color.substr(2, 2), 16);
    var b = parseInt(color.substr(4, 2), 16);
    var yiq = ((r * 299) + (g * 587) + (b * 114)) / 1000;
    return (yiq >= 128) ? '#000000' : '#FFFFFF';
}
textColor function in controller
With this controller action, we can now set the tile text color in the HTML template with ng-style, setting the tile's CSS color attribute to $ctrl.tileTextColor(tile.id) on line 3. Once again now that I am starting to think "the Angular way", I am eliminating a lot of discrete code.
<!-- Populated tile (data loaded) -->
<div id="tile-{{tile.id}}" ng-if="tile.haveData"
        class="tile" ng-class="tile.classes" ng-style="{ 'background-color': $ctrl.tileColor(tile.id), 'color': $ctrl.tileTextColor(tile.id) }"
        style="overflow: hidden"
        draggable="true" ondragstart="tile_dragstart(event);"
        ondrop="tile_drop(event);" ondragover="tile_dragover(event);">
    <div class="dropdown" style="height: 100%">
Setting tile background color and text color in HTML template

Tile Actions

We've had an ellipsis (...) menu at the top right of dashboard tiles all along, visible on hover. Now we'll add actions underneath that menu. Here's the markup for the tile action menu:
<div class="dropdown" style="height: 100%">
    <div class="hovermenu">
        <i class="fa fa-ellipsis-h dropdown-toggle" data-toggle="dropdown" aria-hidden="true"></i>
        <ul class="dropdown-menu" style="margin-left:-130px !important">
            <li><a id="tile-config-{{tile.id}}" href="#" onclick="configureTile(this.id);"><i class="fa fa-gear" aria-hidden="true"></i>  Configure Tile</a></li>
            <li><a href="#" onclick="configureTile('0');"><i class="fa fa-plus-square-o" aria-hidden="true"></i>  Add Tile</a></li>
            <li><a id=tile-remove-{{tile.id}}" href="#" onclick="removeTile(this.id);"><i class="fa fa-remove" aria-hidden="true"></i>  Remove Tile</a></li>
            <li><a id=tile-reset-{{tile.id}}" href="#" onclick="resetDashboard();"><i class="fa fa-refresh" aria-hidden="true"></i>  Reset Dashboard</a></li>
        </ul>
    </div>
Tile Action Menu Markup in HTML Template

This gives us the menu you see below when we click on the ellipsis.

Menu Actions

Now let's look at how each action is implemented.

Configure Tile

The Configure Tile action allows the tile's properties to be edited. When this action is selected, the confguration dialog appears, populated with the tile's properties. As you edit the dialog, the tile changes visually in real-time. Clicking Save commits the changes, saving the updated dialog. Clicking Cancel undoes any changes.

The event handler for Configure Tile is shown below, but all of the work is done in the controller's configureTile function. The page code invokes ctrl.configureTile(id) within a scope.$evalSync(...) call: without $evalSync, scope changes made in the controller's configureTile function would not be detected and the page wouldn't automatically update.
// configureTile : Configure (edit) a tile. Displays configure tile dialog. Value is tile index ('1', '2', ..) or '0' (new tile).

function configureTile(id) {
    var scope = angular.element('#dashboard').scope();
    var ctrl = angular.element('#dashboard').scope().$$childHead.$ctrl;
    scope.$evalAsync(function () {
        return ctrl.configureTile(id);
    });
}
Event hanlder for Configure Tile

Over in the controller, the configureTile function is expecting to be passed a tile Id (1, 2, etc.). If it is passed a zero, that indicates a new tile is being added. In the former case (configuring an existing tile), the 0-based index to the tile is saved in self.configIndex. The self.configuring flag is set to true, indicating tile configuration is in progress--and causing the dialog to be visible.
    // configureTile : Configure (edit) a tile. Displays configure tile dialog. Value is tile index ('1', '2', ..) or '0' (new tile).

    self.configureTile = function (id) {
        if (!id) // if no id specified, cancel configuration tile action
        {
            self.cancelConfigureTile();
        }
        else {
            self.clonedTiles = angular.copy(self.tiles);    // Make a copy of original tile configuration
            self.applied = true;
            if (id == '0') {    // add new tile
                self.configureTileTitle = 'Add New Tile';
                tileIndex = -1;
                var color = '#888888'; 
                $('#configTileColor').val(color);
                $("#configTileColor").spectrum("set", color);
                $('#configTileLink').val('');
                self.configuring = true;
                self.configIndex = self.tiles.length;
                self.tiles.push({
                    id: (self.configIndex+1).toString(),
                    title: 'title',
                    type: 'counter',
                    color: color,
                    dataSource: 'inline',
                    height: '1',
                    width: '1',
                    link: null,
                    classes: 'tile_1_1',
                    columns: null,
                    label: 'label',
                    value: 1,
                    haveData: true
                });
                self.computeLayout();
            }
            else {  // edit existing tile
                var index = parseInt(id.substring(12)) - 1;      // tile-config-1 => 0, tile-config-2 => 1, etc.
                self.configureTileTitle = 'Configure Tile ' + (index+1).toString();
                tileIndex = index;
                self.configIndex = index;
                self.configTile = self.tiles[index];
                if (self.configTile) {
                    self.configuring = true;
                    self.configTileSave = angular.copy(self.configTile);
                    var color = self.configTile.color; 
                    $('#configTileColor').val(color);
                    $("#configTileColor").spectrum("set", color);
                    $('#configTileLink').val(self.configTile.link);
                    var value = '';
                    if (self.configTile.type === 'counter' && self.configTile.dataSource === 'inline') {
                        value = self.configTile.value;
                        $('#configTileValue').val(value);
                    }
                }
            }
        }
        return false;
    };

configureTile function in Controller

In the above, there's some code used to set a color picker control but otherwise you don't see type, title, width, height, etc. being set into the configuration UI. As explained earlier, the reason for this is that the tile properties are bound to the UI automatically by angular directives in the HTML template.

Cancel

If the user clicks Cancel, we need to abandon changes--but because of the 2-way binding, any changes made on the dialog have already been applied to the tile--so how do we handle a Cancel? If you look back above at the conrtroller's configureTile function, the answer is on line 9. When we begin the configuration activity, we make a copy of the entire tile array named clonedTile. This is done using angular.copy, which makes a deep (without references) copy of the tiles array. Because this has been done, the code to cancel (shown below) merely has to copy back the saved copy of the tiles array. It also calls self.computeLayout (to calculate the arrangement of tiles to fit the current window width) and calls deferCreateCharts to have the chart library render any chart tiles.
    // cancelConfigTile : cancel a configure tile action (dismiss tile, clear configuring flags).

    self.cancelConfigureTile = function () {
        if (self.configuring) {
            self.configuring = false;
            if (self.applied) {  // roll back changes from Apply button
                self.tiles = angular.copy(self.clonedTiles);
                self.computeLayout();
                deferCreateCharts();
                self.clonedTiles = null;
                self.applied = false;
            }
        }
    };
cancelConfigureTile function in controller

Save

When Save is clicked, our tiles array is already up to date thanks to the 2-way binding that has been operative all during the editing process. We do, however, need to save the updated layout. That is done with a new call added to the DataService named saveDashboard (line 14).
// saveConfigureTile : Save tile configuration. An Apply can be reverted with a cancel.
// if tileIndex is -1, a new tile is being added.

self.saveConfigureTile = function () {
    var index = tileIndex;

    if (tileIndex == -1) { // new tile
        tileIndex = self.configIndex;
        index = tileIndex;
    }

    self.configTile = self.tiles[index];

    DataService.saveDashboard(self.tiles);

    self.applied = false;
    tileIndex = -1;

    self.updateTileProperties();
    self.computeLayout();
    deferCreateCharts();
    toastr.options = {
        "positionClass": "toast-top-center",
        "timeOut": "1000",
    }
    toastr.info('Dashboard Changes Saved')

    self.configuring = false;
};
saveConfigureTile function in controller

In the SQL Server implementation of the Data Service, saveDashboard makes an Ajax call (line 38) to a SaveDashboard action in the MVC controller. The MVC controller action writes out the dashboard layout and tile properties to the database for the current user. The demo edition of the Data Service also has a saveDashboard function, but it doesn't do anything.
// -------------------- saveDashboard : updates the master layout for tiles (returns tiles array) ------------------

this.saveDashboard = function (newTiles) { 

    var Dashboard = {
        DashboardName: null,
        Username: null,
        Tiles: [],
        Queries: null
    };

    var tile = null;
    var Tile = null;

    // create tile object with properties

    for (var t = 0; t < newTiles.length; t++) {
        tile = newTiles[t];
        Tile = {
            Sequence: t+1,
            Properties: [
                { PropertyName: 'color', PropertyValue: tile.color },
                { PropertyName: 'width', PropertyValue: parseInt(tile.width) },
                { PropertyName: 'height', PropertyValue: parseInt(tile.height) },
                { PropertyName: 'title', PropertyValue: tile.title },
                { PropertyName: 'type', PropertyValue: tile.type },
                { PropertyName: 'dataSource', PropertyValue: tile.dataSource },
                { PropertyName: 'columns', PropertyValue: JSON.stringify(tile.columns) },
                { PropertyName: 'value', PropertyValue: JSON.stringify(tile.value) },
                { PropertyName: 'label', PropertyValue: tile.label },
                { PropertyName: 'link', PropertyValue: tile.link },
                { PropertyName: 'format', PropertyValue: tile.format }
            ]
        };
        Dashboard.Tiles.push(Tile);
    };

    var request = $http({
        method: "POST",
        url: "/Dashboard/SaveDashboard",
        data: JSON.stringify(Dashboard),
        headers : {
            'Content-Type': 'application/json'
        }

    });

    return (request.then(handleSuccess, handleError));
};
saveDashboard function in SQL Server Data Service

Add New Tile

The Add New Tile action adds a new tile to the layout and brings up the configuration dialog. Clicking Save commits the new tile to the layout and saves it. Clicking Cancel abandons the new tile.

Add New Tile uses the same code describes above under Configure Tile. By passing an id of zero, the code in the controller configureTile function knows to add a new tile to the layout.

By the way, Add New Tile and Configure Tile have to deal with the fact that as the user is defining / modifying a tile, some of the tile properties may be incomplete / invalid. In particular, consider what happens when a tile type is changed in the configuration dialog: more likely than not, a valid data source is no longer set that makes sense for that tile type. To combat this, the controller code that responds to changes in type and data source calls a function named tileHasValidTypeAndDataSource(index) before trying to re-fetch data and render the tile. I'm quite sure more needs to be done along these lines--expect an update after more testing and debugging. There, you've been warned!

Remove Tile

The Remove Tile action removes the current tile and saves the updated layout. Your dashboard must have at least one tile, so this action won't do anything if there's only one tile left. 


In the controller, the removeTile function removes the tile from the tiles array using JavaScript's array.splice function. DataService.saveDashboard is called to save the updated dashboard layout. The usual call to computeLayout is then made to recompute the dashboard layout.
// removeTile : Remove a tile.

self.removeTile = function(id) {
    if (!id) return;

    if (self.tiles.length < 2) return; // can't delete all tiles, because we lose the tile menu and have no way to add new tiles

    var index = parseInt(id.substring(12)) - 1;      // tile-remove-1 => 0, tile-remove-2 => 1, etc.

    self.tiles.splice(index, 1);
    self.computeLayout();

    DataService.saveDashboard(self.tiles);

    toastr.options = {
        "positionClass": "toast-top-center",
        "timeOut": "1000",
    }

    toastr.info('Dashboard Tile Deleted');
    return false;
}
removeTile function in controller

There really should be a confirmation dialog for destructive actions such as Remove Tile. One more item to add to our To Do list.

Reset Dashboard

One last action we'll implement is the ability to reset the dashboard. This will remove the user's custom dashboard and return them back to the default layout.

Recall that our database scheme is to have a default dashboard in the database, but also the ability for saving a custom dashboard for a user: all of the cases above where we've added / configured / removed a tile causes a custom dashboard layout to be saved for the user.

When our code loads a dashboard for a user, it tries to find a custom one for the current user's username. If it can't find that, it uses the default dashboard. All it takes for a dashboard reset, then, is to delete the user's custom saved dashboard. In the Angular controller, the resetDashboard function invokes the Data Service resetDashboard function; and then re-loads the user's dashboard.
// resetDashboard : reset user's dashboard

self.resetDashboard = function () {
    Promise.resolve(DataService.resetDashboard()).then(
        function (response) {
            self.LoadDashboard();
        });
}
resetDashboard function in controller

The Data Service's resetDashboard function in turn calls an MVC action that removes the custom dashboard from the database.
// resetDashboard : reset user's dashboard

this.resetDashboard = function () {
    var request = $http({
        method: "GET",
        url: "/Dashboard/DashboardReset/",
    });
}
resetDashboard function in SQL DataService

Drag and Drop Persistence

In addition to the tile actions we've covered that persist a user's custom dashboard layout to the database, there's one other way we have to modify the layout: through drag-and-drop. Several posts back we developed the capability to re-arrange tiles through dragging. We've updated the controller's dragTile method to persist the dashboard changes after a tile has been moved. This is done by calling DataService.saveDashboard, just as with the other tile actions described earlier.

New MVC Controller Actions

We've mentioned a few new MVC actions but haven't shared the back-end C# code yet, so let's review it now.

SaveDashboard

SaveDashboard saves a dashboard layout for the current user in the SQL Server database. It first deletes any existing dashboard layout set of records for the username, then adds a new set.
        // /Dashboard/SaveDashboard (POST) tiles .... save updated dashboard for user.

        [HttpPost]
        public void SaveDashboard(Dashboard dashboard)
        {
            try
            {
                String username = CurrentUsername();

                DeleteDashboard();  // Delete prior saved dashboard (if any) for user.

                // Check whether an existing dashboard is saved for this user. If so, delete it.

                int dashboardId = -1;

                using (SqlConnection conn = new SqlConnection(System.Configuration.ConfigurationManager.AppSettings["Database"]))
                {
                    conn.Open();

                    // Add dashboard layout root record

                    String query = "INSERT INTO DashboardLayout (DashboardName, Username, Priority) VALUES (@DashboardName, @Username, 2); SELECT SCOPE_IDENTITY();";

                    using (SqlCommand cmd = new SqlCommand(query, conn))
                    {
                        cmd.Parameters.AddWithValue("@DashboardName", "Home");
                        cmd.Parameters.AddWithValue("@Username", username);
                        using (SqlDataReader reader = cmd.ExecuteReader())
                        {
                            if (reader.Read())
                            {
                                dashboardId = Convert.ToInt32(reader[0]);
                            }
                        }
                    }

                    if (dashboardId!=-1) // If root record added and we have an id, proceed to add child records
                    {
                        // Add DashboardLayoutTile records.

                        int sequence = 1;
                        foreach (Tile tile in dashboard.Tiles)
                        {
                            query = "INSERT INTO DashboardLayoutTile (DashboardId, Sequence) VALUES (@DashboardId, @Sequence)";

                            using (SqlCommand cmd = new SqlCommand(query, conn))
                            {
                                cmd.Parameters.AddWithValue("@DashboardId", dashboardId);
                                cmd.Parameters.AddWithValue("@Sequence", sequence);
                                cmd.ExecuteNonQuery();
                            }
                            sequence++;
                        } // next tile

                        // Add DashboardLayoutTileProperty records.

                        sequence = 1;
                        foreach (Tile tile in dashboard.Tiles)
                        {
                            query = "INSERT INTO DashboardLayoutTileProperty (DashboardId, Sequence, PropertyName, PropertyValue) VALUES (@DashboardId, @Sequence, @Name, @Value)";

                            using (SqlCommand cmd = new SqlCommand(query, conn))
                            {
                                cmd.Parameters.AddWithValue("@DashboardId", dashboardId);
                                cmd.Parameters.AddWithValue("@Sequence", sequence);
                                cmd.Parameters.AddWithValue("@Name", "color");
                                if (tile["color"] == null)
                                {
                                    cmd.Parameters.AddWithValue("@Value", DBNull.Value);
                                }
                                else
                                {
                                    cmd.Parameters.AddWithValue("@Value", tile["color"]);
                                }
                                cmd.ExecuteNonQuery();

                                cmd.Parameters.Clear();
                                cmd.Parameters.AddWithValue("@DashboardId", dashboardId);
                                cmd.Parameters.AddWithValue("@Sequence", sequence);
                                cmd.Parameters.AddWithValue("@Name", "height");
                                if (tile["height"] == null)
                                {
                                    cmd.Parameters.AddWithValue("@Value", DBNull.Value);
                                }
                                else
                                {
                                    cmd.Parameters.AddWithValue("@Value", tile["height"]);
                                }
                                cmd.ExecuteNonQuery();

                                cmd.Parameters.Clear();
                                cmd.Parameters.AddWithValue("@DashboardId", dashboardId);
                                cmd.Parameters.AddWithValue("@Sequence", sequence);
                                cmd.Parameters.AddWithValue("@Name", "width");
                                if (tile["width"] == null)
                                {
                                    cmd.Parameters.AddWithValue("@Value", DBNull.Value);
                                }
                                else
                                {
                                    cmd.Parameters.AddWithValue("@Value", tile["width"]);
                                }
                                cmd.ExecuteNonQuery();

                                cmd.Parameters.Clear();
                                cmd.Parameters.AddWithValue("@DashboardId", dashboardId);
                                cmd.Parameters.AddWithValue("@Sequence", sequence);
                                cmd.Parameters.AddWithValue("@Name", "type");
                                if (tile["type"] == null)
                                {
                                    cmd.Parameters.AddWithValue("@Value", DBNull.Value);
                                }
                                else
                                {
                                    cmd.Parameters.AddWithValue("@Value", tile["type"]);
                                }
                                cmd.ExecuteNonQuery();

                                cmd.Parameters.Clear();
                                cmd.Parameters.AddWithValue("@DashboardId", dashboardId);
                                cmd.Parameters.AddWithValue("@Sequence", sequence);
                                cmd.Parameters.AddWithValue("@Name", "title");
                                if (tile["title"] == null)
                                {
                                    cmd.Parameters.AddWithValue("@Value", DBNull.Value);
                                }
                                else
                                {
                                    cmd.Parameters.AddWithValue("@Value", tile["title"]);
                                }
                                cmd.ExecuteNonQuery();

                                cmd.Parameters.Clear();
                                cmd.Parameters.AddWithValue("@DashboardId", dashboardId);
                                cmd.Parameters.AddWithValue("@Sequence", sequence);
                                cmd.Parameters.AddWithValue("@Name", "dataSource");
                                if (tile["dataSource"] == null)
                                {
                                    cmd.Parameters.AddWithValue("@Value", DBNull.Value);
                                }
                                else
                                {
                                    cmd.Parameters.AddWithValue("@Value", tile["dataSource"]);
                                }
                                cmd.ExecuteNonQuery();

                                cmd.Parameters.Clear();
                                cmd.Parameters.AddWithValue("@DashboardId", dashboardId);
                                cmd.Parameters.AddWithValue("@Sequence", sequence);
                                cmd.Parameters.AddWithValue("@Name", "label");
                                if (tile["label"] == null)
                                {
                                    cmd.Parameters.AddWithValue("@Value", DBNull.Value);
                                }
                                else
                                {
                                    cmd.Parameters.AddWithValue("@Value", tile["label"]);
                                }
                                cmd.ExecuteNonQuery();

                                cmd.Parameters.Clear();
                                cmd.Parameters.AddWithValue("@DashboardId", dashboardId);
                                cmd.Parameters.AddWithValue("@Sequence", sequence);
                                cmd.Parameters.AddWithValue("@Name", "columns");
                                if (tile["columns"] == null)
                                {
                                    cmd.Parameters.AddWithValue("@Value", DBNull.Value);
                                }
                                else
                                {
                                    cmd.Parameters.AddWithValue("@Value", tile["columns"]);
                                }
                                cmd.ExecuteNonQuery();

                                cmd.Parameters.Clear();
                                cmd.Parameters.AddWithValue("@DashboardId", dashboardId);
                                cmd.Parameters.AddWithValue("@Sequence", sequence);
                                cmd.Parameters.AddWithValue("@Name", "value");
                                if (tile["value"] == null)
                                {
                                    cmd.Parameters.AddWithValue("@Value", DBNull.Value);
                                }
                                else
                                {
                                    cmd.Parameters.AddWithValue("@Value", tile["value"]);
                                }
                                cmd.ExecuteNonQuery();

                                cmd.Parameters.Clear();
                                cmd.Parameters.AddWithValue("@DashboardId", dashboardId);
                                cmd.Parameters.AddWithValue("@Sequence", sequence);
                                cmd.Parameters.AddWithValue("@Name", "link");
                                if (tile["link"] == null)
                                {
                                    cmd.Parameters.AddWithValue("@Value", DBNull.Value);
                                }
                                else
                                {
                                    cmd.Parameters.AddWithValue("@Value", tile["link"]);
                                }
                                cmd.ExecuteNonQuery();
                            }
                            sequence++;
                        } // next tile
                    }

                    conn.Close();
                } // end SqlConnection
            }
            catch(Exception ex)
            {
                Console.WriteLine("EXCEPTION: " + ex.Message);
            }
        }

        // DeleteDashboard : Delete user's saved custom dashboard.

        private void DeleteDashboard()
        {
            try
            {
                String username = CurrentUsername();

                // Check whether an existing dashboard is saved for this user. If so, delete it.

                int dashboardId = -1;

                using (SqlConnection conn = new SqlConnection(System.Configuration.ConfigurationManager.AppSettings["Database"]))
                {
                    conn.Open();

                    // Load the dashboard.
                    // If the user has a saved dashboard, load that. Otherwise laod the default dashboard.

                    String query = "SELECT TOP 1 DashboardId FROM DashboardLayout WHERE DashboardName='Home' AND Username=@Username";

                    using (SqlCommand cmd = new SqlCommand(query, conn))
                    {
                        cmd.CommandType = System.Data.CommandType.Text;
                        cmd.Parameters.AddWithValue("@Username", CurrentUsername());
                        using (SqlDataReader reader = cmd.ExecuteReader())
                        {
                            if (reader.Read())
                            {
                                dashboardId = Convert.ToInt32(reader["DashboardId"]);
                            }
                        }
                    }

                    if (dashboardId != -1) // If found a dashboard...
                    {
                        // Delete dashboard kayout tile property records

                        query = "DELETE DashboardLayoutTileProperty WHERE DashboardId=@DashboardId";

                        using (SqlCommand cmd = new SqlCommand(query, conn))
                        {
                            cmd.CommandType = System.Data.CommandType.Text;
                            cmd.Parameters.AddWithValue("@DashboardId", dashboardId);
                            cmd.ExecuteNonQuery();
                        }

                        // Delete dashboard layout tile records

                        query = "DELETE DashboardLayoutTileProperty WHERE DashboardId=@DashboardId";

                        using (SqlCommand cmd = new SqlCommand(query, conn))
                        {
                            cmd.CommandType = System.Data.CommandType.Text;
                            cmd.Parameters.AddWithValue("@DashboardId", dashboardId);
                            cmd.ExecuteNonQuery();
                        }

                        // Delete dashboard layout record

                        query = "DELETE DashboardLayout WHERE DashboardId=@DashboardId";

                        using (SqlCommand cmd = new SqlCommand(query, conn))
                        {
                            cmd.CommandType = System.Data.CommandType.Text;
                            cmd.Parameters.AddWithValue("@DashboardId", dashboardId);
                            cmd.ExecuteNonQuery();
                        }
                    }
                    conn.Close();
                } // end SqlConnection
            }
            catch (Exception ex)
            {
                Console.WriteLine("EXCEPTION: " + ex.Message);
            }
        }

SaveDashboard Action in MVC Controller

DashboardReset

The DashboardReset action action deletes the user's saved custom dashboard (if any).

        // DashboardReset ... reset user to default dashboard (by deleting their saved custom dashboard).

        [HttpGet]
        public JsonResult DashboardReset()
        {
            try
            {
                DeleteDashboard();
                return Json(true, JsonRequestBehavior.AllowGet);
            }
            catch (Exception ex)
            {
                Console.WriteLine("EXCEPTION: " + ex.Message);
                return Json(false, JsonRequestBehavior.AllowGet);
            }
        }
DashboardReset Action in MVC Controller

Summary

Today we achieved the following:

  • Added a tile configuration UI
  • Expanded the tile color palette
  • Used Angular directives for tile editing with real-time visual changes
  • Added dashboard layout persistence with new Data Service functions and MVC Controller actionss
  • Added tile menu actions to Configure Tile, Add New Tile, Remove Tile, and Reset Dashboard

After 5 posts, we now have a dashboard that works! However, we can't quite claim completion yet. For one thing, all of the above is brand new and it needs testing and debugging. After some of that, we'll look to polish what we have and perhaps add some more functionality.

Download Source 
Download Zip
https://drive.google.com/file/d/0B4734rvcufGGRC1ZenItNUNwNTA/view?usp=sharing

Next: Part 6 : Admin Tile Actions and Confirmation Dialog





No comments: