CPU Load Gadget for EPiServer CMS July CTP

by: Roger Wirz

The new EPiServer CMS Shell framework allows you to create any kind of fancy Gadgets that will be shown in the Site Center part of EPiServer CMS (the July CTP release). After playing around with this for a couple of hours (great fun!!), I found some parts I missed, like the possibility to separate my Gadgets in a module package and some simple auto refresh functions. This blog is a walk through, describing how to create your first Gadget separated from the standard Views. I can also serve as a appetizer for anyone wanting to have some fun with the CTP.

The Gadget we shall build is a Gadget showing the current server CPU load. Simple but effectual.

image  image

image

Set up the project

Let's get started. First of all we create a new ASP.NET MVC WebApplication called Gadgets and place it in our site folder (a new subfolder called Gadgets should have been created). Remove all content in the folders Content, Scripts, Controllers, Models and Views and remove the files Default.aspx, Global.asax and Web.config. It should look like this when you are done.

image

Now, add a reference to System.Web.MVC and EPiServer.Shell and change the compilation path to ..\bin\. Finally, register the assembly in web.config to tell EPiServer to check for Gadgets within this assembly.

<episerver.shell>
    <plugInAssemblies autoResolve="false">
          <add assemblyName="Gadgets"/>

 

Management of a separated Gadget folder

If we should have placed our new Gadget within the default location of the site, in the "root" Views folder, we could have started to create our new Gadget right now. But we want to create it in a separate "Gadget" folder to make deployment easier. To get this to work we need to add a Route to the RouteTable and also add our own ViewEngine. The code below needs to execute at application start and because of this we add it into a HttpModule.

The file GadgetViewEngineModule.cs:

using System.Web;
using System.Web.Mvc;
using System.Web.Routing;

namespace Gadgets
{
    /// <summary>
    /// This HttpModule needs to be added to the site to map MVC routing correctly.
    /// Add a "add "row in the httpModules-section to register the module.
    /// </summary>
    public class GadgetViewEngineModule : IHttpModule
    {
        public static bool _isRegistered  = false;

        public void Init(HttpApplication app)
        {
            if (!_isRegistered)
            {
                // Map the MVC routing
                // - To fetch MVC calls  to the site other than EPiServer internal
                RouteTable.Routes.MapRoute("GadgetRoute", "{controller}/{action}", new { });

                // Add a ViewLocationFormats for /SamplePackage/Views
                // - To allow View:s to be placed in a subfolder like Gadgets
                ViewEngines.Engines.Add(new Gadgets.GadgetViewEngine());

                _isRegistered = true;
            }
        }

        public void Dispose() { }
    }
}

The file GadgetViewEngine.cs:

using System.Web.Mvc;

namespace Gadgets
{
    public class GadgetViewEngine : VirtualPathProviderViewEngine
    {
        public GadgetViewEngine()
        {
            ViewLocationFormats = new[] {  
                "~/Gadgets/Views/{1}/{0}.ascx",  
            };
        }

        protected override IView CreatePartialView(ControllerContext controllerContext, string partialPath)
        {
            return new WebFormView(partialPath, null);
        }

        protected override IView CreateView(ControllerContext controllerContext, string viewPath, string masterPath)
        {
            return new WebFormView(viewPath, masterPath);
        }
    }
}

When these files have been added and compiled you can add the Http Module to web.config.

<system.web>
    <httpModules>
        <add name="GadgetsMVCRouting" type="Gadgets.GadgetViewEngineModule, Gadgets" />

 

Note: This blog post only applies to the first CTP of EPiServer CMS released in July 2009. Routing and ViewEngines will hopefully be unnecessary in upcoming releases.

 

Creating the Controller

First of all, let us take a look at the Controller as it will look when we are done. For convenience reasons it should be place in the Controllers folder.

The file CPUController.cs:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Web.Mvc;
using EPiServer.Shell.Gadgets;
using EPiServer.Shell.Gadgets.Contracts;
using Gadgets.Models;

namespace Gadgets
{
    [EPiServer.Shell.Gadgets.Gadget("CPU Load")]
    public class CPULoadController : Controller
    {
        // CPU cache
        static DateTime _cacheTimeout = DateTime.MinValue;
        static float _cpuUsage = 0;

        // A simple, non persistent store with no support for load balancing
        static Dictionary<Guid, int> store = new Dictionary<Guid, int>();
        static System.Diagnostics.PerformanceCounter counter = null;

        public ActionResult Index(Guid gadgetId)
        {
            CpuSettings cpuSettings = new CpuSettings();

            // Avoid overload of CPU-check when the Gadget refreshes itself.
            // E.g. with a Refresh rate of 1 second,  100 user with this Gadget
            // displayed on screen gives 100 hps -> 100 checks of the CPU per
            // second. That is without the cache...
            // With the cache, the CPU is checked at the most 2 times every second.
            if (_cacheTimeout < DateTime.Now)
            {
                _cpuUsage = CpuUsageValue();
                _cacheTimeout = DateTime.Now.AddMilliseconds(500);
            }

            cpuSettings.GadgetId = gadgetId;
            cpuSettings.FillingHeight = Convert.ToInt32((100 - _cpuUsage) * 1.5);
            cpuSettings.CPULoadHeight = Convert.ToInt32(_cpuUsage * 1.5);
            cpuSettings.CPULoad = Convert.ToInt32(_cpuUsage);
            cpuSettings.RefreshTime = GetRefreshTime(gadgetId);

            return View(cpuSettings);
        }

        [GadgetAction(ActionType.Menu, Text = "Edit Settings")]
        public ActionResult Configure(Guid gadgetId)
        {
            CpuSettings model = new CpuSettings { RefreshTime = GetRefreshTime(gadgetId) };
            return View(model);
        }

        public ActionResult StoreConfiguration([Bind]CpuSettings model)
        {
            SaveRefreshTime(model.GadgetId, model.RefreshTime);

            return Json(true);
        }

        private int GetRefreshTime(Guid gadgetId)
        {
            if (store.ContainsKey(gadgetId))
                    return store[gadgetId];

            return 1200;
        }

        private void SaveRefreshTime(Guid gadgetId, int RefreshTime)
        {
            CPULoadController.store[gadgetId] = RefreshTime;
        }

        private float CpuUsageValue()
        {
            if (counter == null)
                counter = new PerformanceCounter("Processor", "% Processor Time", "_Total");

            return counter.NextValue(); ;
        }
    }

In the Action "Index" the model will be filled with data used rendering the Index View and in the Action "Configure" the model get just the settings data - RefreshTime - used by the Configure View.

One thing to notice is that there is a simple Cache built in. The reason for doing this is to avoid a overload of the CPU checking (or anything else done by a Controller). Let us take one example. If this Gadget gets popular it might be shown on the Site Center displayed by 100 people in the office. If those 100 choose to set a refresh time at 600 ms it will mean almost 150 calls per second to the site and in extention to the counter.NextValue() part. A wild guess is that they all will get either strange values or 100% CPU all the time beacuse of the load. And nothing else will be delivered by the site beacuse it is busy telling everyone that it is busy.  Adding (micro) Caching is a good way to avoid such problems. It reduces the load to 2 calls per second but all the users will still get (almost) real time data.

 

Creating the Model

Now that we have the Control in place, let's create the Model and place it in the Models folder.

The file CpuSettings.cs:

using System;

namespace Gadgets.Models
{
    public class CpuSettings
    {
        public Guid GadgetId { get; set; }
        public int CPULoad { get; set; }
        public int CPULoadHeight { get; set; }
        public int FillingHeight { get; set; }
        public int RefreshTime { get; set; }
    }
}

This model should perhaps have been divided into two models but for simplicity I have put both settings data (Refresh Time) and model data for the View, in the same model.

 

Creating the Configure View

This View is displayed when the user choose "Edit Settings" in the Site Center (or actually when the site gets a request for /CPULoad/Configure).

The file Configure.ascx:

<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<CpuSettings>" %>
<%@ Import Namespace="Gadgets.Models" %>
<%@ Import Namespace="EPiServer.Shell.Extensions" %>

<% Html.BeginGadgetForm("StoreConfiguration"); %>
    <div style="padding:8px;">
        <label>
            <span>Refresh rate
            <ul>
                <li>
                    <label><%= Html.RadioButtonWithUniqueID("RefreshTime", 600, (Model.RefreshTime == 600))%>Fast (600 ms)</label>
                </li>
                <li>
                    <label><%= Html.RadioButtonWithUniqueID("RefreshTime", 1200, (Model.RefreshTime == 1200))%>Medium (1 200ms)</label>
                </li>
                <li>
                    <label><%= Html.RadioButtonWithUniqueID("RefreshTime", 4000, (Model.RefreshTime == 4000))%>Slow (4 s)</label>
                </li>
            </ul>
            </span>
        </label>
        <div class="epi-buttonRow">
            <%= Html.GadgetOkCancelButtons()%>
        </div>
    </div>

<% Html.EndForm(); %>

 

The System.Web.Mvc.ViewUserControl<CpuSettings> allows us to work with the model data sent to the View. The Html.BeginGadgetForm gives us a fully functional Form start to encapsulate the part we want to send back to the server. It will need a closing tag as well - <% Html.EndForm(); %>.

The name of the Radio Buttons, "RefreshTime", will be used as a reference when the form is serialized and sent back to the server. The form content will be sent and received and "handled" by the following Controller method "public ActionResult StoreConfiguration([Bind]CpuSettings model)"

Finally the <%= Html.GadgetOkCancelButtons()%> will give us a OK and a Cancel button submitting the form or reloading the Index view again.

image

It can be noted that the setting in this implementation will not be saved permanently, nor will it cover load balancing. It will also not support different settings for different user but that could be easily fixed just by adding the user name to the part where the settings is stored (CPULoadController.store[gadgetId + UserName] = RefreshTime;).

 

Creating the Index View

Now we have come to the last but most important part of this exercise - the Index View. It will be displayed as soon as the Controller gets a call to /CPULoad/Index. And that is when the Site Center page is loaded or when client side code sends a request for refreshing the content (through jQuery).

The file: Index.ascx:

<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<CpuSettings>" %>
<%@ Import Namespace="Gadgets.Models" %>
<style>
#displaycontainer
{
    background-color:#454545;
    margin:10px;
    padding-top:10px;
    height:200px;
    text-align:center;
}

#cpuloaddigits
{
    width:40%;
    height:155px;
    float:left;
    font-size:80px;
}

#cpuloadfilling, #cpu
{
    margin-left:45%;
    margin-right:1em;
}

#cpuloadfilling
{
    background-color:#454545;   
    height:<%= Model.FillingHeight %>px;
}

#cpuload
{
    background-color:#858585;
    height:<%= Model.CPULoadHeight %>px;
}
</style>

<% Html.BeginForm(new { Action = "Index" }); %>      

    <div id="displaycontainer">
        <div id="cpuloadheading">
            <h2>CPU Usage</h2>    
        </div>
        <div id="cpuloaddigits" ><%= Model.CPULoad %> %</div>
        <div id="cpuloadfilling"></div>
        <div id="cpuload"></div>
    </div>

    <script type="text/javascript">
       (function($) {
           $(document).ready(function() {
             var reload<%= Model.GadgetId.ToString().Replace("-","_") %> = function()
             {
                 $.post('/CPULoad/Index?gadgetId=<%= Model.GadgetId %>', function(data) {
                    $("#displaycontainer").closest(".epi-gadgetContent").html(data);
                    })
             }
             setTimeout(reload<%= Model.GadgetId.ToString().Replace('-','_') %>, <%= Model.RefreshTime %>);
           });
        }(epiJQuery));
    </script>    

<% Html.EndForm(); %>

The first section is just a Style section. It could as well have been put in a separate css-file but then we would have to build a client side update of the div-height and it had just made things more complex.

The visual effect is made by regulary updating the HTML inside the "epi-gadgetContent"-section from the client. Each time the View is rendered at the client, setTimeout is activated and will lead to a post back after a specified time. The JQuery part will then replace the content with the "data" received through the request (post) to /CPULoad/Index?gadgetId=nnnn. The query parameter will make it possible for the controller to identify the correct Gadget (public ActionResult Index(Guid gadgetId)) .

Because more than one instance of this Gadget can be displayed at the same time on the same page, we need to make the function name unique. We do this simply by adding the gadgetId to the function name (replacing "-" with "_").

Summary

After you have completed this exercise you should have a project similar to this, in a subfolder of the site folder:

image

And the Gadget should look something similar to this:

image

I hope that you have gotten some inspiration building Gadgets and also hopefully gotten some ideas of how to make deployment easiers, being able to separate your Gadgets into separate projects and folders.

Finally, if you want to learn more about JQuery, you could take a look at the Video at this link http://is.gd/jddQ

08 July 2009


Comments

  1. On the subject of load balancing, why not get the highest availability while not getting caught in high prices? Kemp’s got some great load balancers that are low priced and high in quality: http://www.kemptechnologies.com/?utm_source=blog&utm_medium=pv&utm_content=zs&utm_campaign=home
Post a comment    
User verification Image for user verification  
Roger Wirz

About me

I was born 1964. I live with my wife and my two children in Upplands Väsby north of Stockholm.

I started to work at EPiServer AB (ElektroPost) 1998 as Project Leader and I am today working with Product Management.

Syndications


Archive


Tag cloud

EPiTrace logger