Click here to Skip to main content
Click here to Skip to main content

CodeStash - A useful (hopefully) tool for devs II

, , 21 Mar 2012 CPOL
Rate this:
Please Sign up or sign in to vote.
A distributed codesnippet storage tool : Part II

CodeStash Article Listings

Table of Contents

Introduction

Last time we talked about what CodeStash actually is, and some of the high level aspects of the web site and showed some screen shots. This time we are going to take a deep dive into each of the pages that make up the CodeStash web site. So lets carry on to look at these pages, which are all discussed below. 

Low Level Architecture

The general approach I am going to take while going through the seperate pages of the CodeStash web site, is that I am going to follow the same pattern eaxch time, where the pattern will be as follows:

  1. I will show a screen shot of the given page (as we saw in the 1st article)
  2. I will then talk about how the page works and what functionality it is trying to provide
  3. I will then show the most relevant parts of the Controller
  4. I will then show the most relevant parts of the CsHtml file
  5. I will then show the most relevant parts of the JavaScript file

Login

The idea behind CodeStash was to offer a flexible login model, which would allow users to either register or use some existing OpenId provider (such as Google/AOL etc etc) to login.

I also wanted the OpenId login to work in conjunction with the standard ASP .NET forms authentication mechanism. There really is not that much information available about how to do that, especially if the web is not you main skill (which is the case with me).

There is an excellent starting project on how to integrate OpenId and standard ASP .NET forms authentication into an ASP MVC 3 application, this was used as the basis for CodeStash which is available at :
http://weblogs.asp.net/haithamkhedre/archive/2011/03/13/openid-authentication-with-asp-net-mvc3-dotnetopenauth-and-openid-selector.aspx

In order to carry out the OpenId authentication I have used the excellent free .NET library "DotNetOpenAuth.dll".

I have expanded apon this quite a lot, and what you now see in CodeStash works as follows:

Any standard ASP .NET MVC Controller action that requires OpenId authorization will simply use the standard [Authorize] attribute which is enough to make it link in within this OpenId/forms authentication code.

This starter project provides the ability to do:

  1. Login using OpenId
  2. Register as a new user (without using an existing OpenId login)

The 1st step of the login process is that the user logs in to their OpenId provider, once that is done, the user is given an OpenId token by their provider. The next step is that the user information is associated with the obtained OpenId token, and stored as a standard ASP .NET Membership user. At this point the newly created ASP .NET Membership users details are also used to create a authentication cookie is stored for the forms authentication mechanism, and the user is then considered to be authorized and will be allowed access to the rest of the CodeStash web site.

When the user is considered to be authorized by the OpenId provider a new ASP .NET Membership user is created, along with a encrypted CodeStash token, which ideally would be emailed to the user, that however has not been done. So a compromise is that we show the authenticated user a standard ASP .NET MVC View with the encrypted CodeStash token shown, and ask them to write it down ready to enter into the VS2010 addin host app, as described in Pete's upcoming articles.

Once this initial login is done and a ASP .NET Membership user has been created and subsequent login by the user will only require the user to click their OpenId provider button in future, and the OpenId token will be obtained and the user will be able to login using 1 simple click.

The OpenId library that I am using "DotNetOpenAuth.dll" can also be configured through a custom config section which is shown below.

<configSections>
  <section name="dotNetOpenAuth" type="DotNetOpenAuth.Configuration.DotNetOpenAuthSection" requirePermission="false" allowLocation="true" />
</configSections>

<!--OpenId settings-->
<dotNetOpenAuth>
  <openid>
    <relyingParty>
      <security requireSsl="false" />
      <behaviors>
        <!-- The following OPTIONAL behavior allows RPs to use SREG only, but be compatible  
                                with OPs that use Attribute Exchange (in various formats). -->
        <add type="DotNetOpenAuth.OpenId.Behaviors.AXFetchAsSregTransform, DotNetOpenAuth" />
      </behaviors>
    </relyingParty>
  </openid>
  <messaging>
    <untrustedWebRequest>
      <whitelistHosts>
        <!-- since this is a sample, and will often be used with localhost -->
        <add name="localhost" />
      </whitelistHosts>
    </untrustedWebRequest>
  </messaging>
  <!-- Allow DotNetOpenAuth to publish usage statistics to library authors to improve the library. -->
  <reporting enabled="true" />
</dotNetOpenAuth>

So that is the general idea behind it, are you ready to dive a little deeper? Come on it will be fun.

The login process is conducted in the AccountController, and works as follows:

  1. Page that needs authorization (using the standard AuthorizeAttribute) is requested.
  2. Redirected to GET AccountController Logon action, if the user is not authenticated.
  3. Logon view is shown, which has all the OpenID provider links on them, where the HTML form is set to POST to the AccountController POST Logon action.
  4. User picks an OpenID provider, and clicks it, which calls the JavaScript, which really just results in the OpenID provider string being stored and a POST request being made to the AccountController Logon action.
  5. The AccountControllers POST Logon action does two things:
    1. It adds on a ClaimRequest, where it asks the OpenID provider to include Email/FullName in the data that will be included with a successful response from the OpenID provider.
    2. Then redirects the OpenID provider web site (via magic of DotNetOpenAuth.dll), where the user enters their details.
  6. If the user enters valid credentials at the OpenID provider's web site, they are redirected to the default Logon action on AccountController (via magic of DotNetOpenAuth.dll), at which point, it will examine the result from the OpenID provider IAuthenticationResponse response, which is available by calling the OpenIdRelyingParty type's GetResponse() method. If the response is found to be AuthenticationStatus.Authenticated, the user is deemed validated, and then more details about the user can be requested from the OpenID provider's response, which is accomplished using response.GetUntrustedExtension<ClaimsResponse> / response.GetExtension<ClaimsResponse>, where ClaimsResponse is the response that matches the ClaimsResponse that we asked the OpenID provider to include in the AccountController's POST Logon action in Step 5. We can then use the ClaimsResponse to obtain the user's Email and FullName from the OpenID provider's ClaimsResponse.

And here is the most relevant parts of the AccountController's OpenId methods

using System;
using System.Web.Mvc;
using System.Web.Security;
using CodeStash.Models.Security;
using CodeStash.Services;
using DotNetOpenAuth.Messaging;
using DotNetOpenAuth.OpenId;
using DotNetOpenAuth.OpenId.Extensions.AttributeExchange;
using DotNetOpenAuth.OpenId.Extensions.SimpleRegistration;
using DotNetOpenAuth.OpenId.RelyingParty;

namespace CodeStash.Controllers
{
    public class AccountController : Controller
    {
        private static OpenIdRelyingParty openid = new OpenIdRelyingParty();
        private IFormsAuthenticationService formsService;
        private IMembershipService membershipService;
        private ILoggerService loggerService;

        public AccountController(   IFormsAuthenticationService formsService, 
                                    IMembershipService membershipService,
                                    ILoggerService loggerService)
        {
            this.formsService = formsService;
            this.membershipService = membershipService;
            this.loggerService = loggerService;
        }


        public ActionResult LogOn()
        {
            loggerService.Info("LogOn GET");
            return View();
        }
        

        [HttpPost]
        [ValidateAntiForgeryToken(Salt = "LogOn")]
        public ActionResult LogOn(LogOnModel model, string returnUrl)
        {
            if (ModelState.IsValid)
            {
                if (membershipService.ValidateUser(model.UserName, model.Password))
                {
                    formsService.SignIn(model.UserName, model.RememberMe);
                    if (Url.IsLocalUrl(returnUrl))
                    {
                        loggerService.Info(string.Format("LogOn : Sucessful redirecting to {0}", returnUrl));
                        return Redirect(returnUrl);
                    }
                    else
                    {
                        Session["EncryptedPasswordForUserToWriteDown"] = null;
                        loggerService.Error("LogOn : UnSucessful logon redirecting to Home");
                        return RedirectToAction("Index", "Home");
                    }
                }
                else
                {
                    loggerService.Error("LogOn : The user name or password provided is incorrect.");
                    ModelState.AddModelError("", "The user name or password provided is incorrect.");
                }
            }

            // If we got this far, something failed, redisplay form
            return View(model);
        }



        [ValidateInput(false)]
        public ActionResult Authenticate(string returnUrl)
        {
            var response = openid.GetResponse();
            if (response == null)
            {
                //Let us submit the request to OpenID provider
                Identifier id;
                if (Identifier.TryParse(Request.Form["openid_identifier"], out id))
                {
                    try
                    {
                        var request = openid.CreateRequest(Request.Form["openid_identifier"]);


                        var claim = new ClaimsRequest
                        {
                            Email = DemandLevel.Require,
                            Nickname = DemandLevel.Require,
                            FullName = DemandLevel.Request,
                        };

                        var fetch = new FetchRequest();
                        fetch.Attributes.AddRequired(WellKnownAttributes.Name.First);
                        fetch.Attributes.AddRequired(WellKnownAttributes.Name.Last);

                        request.AddExtension(claim);
                        request.AddExtension(fetch);



                        return request.RedirectingResponse.AsActionResult();
                    }
                    catch (ProtocolException ex)
                    {
                        ViewBag.Message = ex.Message;
                        return View("LogOn");
                    }
                }

                ViewBag.Message = "Invalid identifier";
                return View("LogOn");
            }

            //Let us check the response
            switch (response.Status)
            {

                case AuthenticationStatus.Authenticated:
                    LogOnModel lm = new LogOnModel();
                    lm.OpenID = response.ClaimedIdentifier;

                    var claim = response.GetExtension<ClaimsResponse>();
                    var fetch = response.GetExtension<FetchResponse>();
                    var nick = response.FriendlyIdentifierForDisplay;
                    var email = string.Empty;

                    if (claim != null)
                    {
                        nick = string.IsNullOrEmpty(claim.Nickname) ? claim.FullName : claim.Nickname;
                        email = claim.Email;
                    }

                    if (string.IsNullOrEmpty(nick) && fetch != null &&
                        fetch.Attributes.Contains(WellKnownAttributes.Name.First) &&
                        fetch.Attributes.Contains(WellKnownAttributes.Name.Last))
                    {
                        nick = fetch.GetAttributeValue(WellKnownAttributes.Name.First) + " " +
                               fetch.GetAttributeValue(WellKnownAttributes.Name.Last);
                    }

                    MembershipUser user = membershipService.GetUserByOpenId(lm.OpenID);
                    Tuple<MembershipCreateStatus, String> resultsAndPassword = null;

                    if (user == null)
                    {
                        resultsAndPassword = membershipService.CreateUser(nick, email, lm.OpenID);
                        Session["EncryptedPasswordForUserToWriteDown"] = resultsAndPassword.Item2;
                        user = membershipService.GetUserByOpenId(lm.OpenID);
                    }
                    else
                    {
                        Session["EncryptedPasswordForUserToWriteDown"] = user.GetPassword();
                    }

                    //check if user is still empty, which means we have now managed to authenticate via OpenId
                    //and store in database
                    if (user != null)
                    {
                        lm.UserName = user.UserName;
                        formsService.SignIn(user.UserName, false);
                        Session["User"] = user;
                        
                        return RedirectToAction("Index", "Home");
                    }
                    else
                    {
                        return View("LogOn", lm);
                    }

                case AuthenticationStatus.Canceled:
                    ViewBag.Message = "Canceled at provider";
                    return View("LogOn");
                case AuthenticationStatus.Failed:
                    ViewBag.Message = response.Exception.Message;
                    return View("LogOn");
            }

            return new EmptyResult();
        }
    }
}

The login mechanism works in conjunction with the standard ASP MVC AuthorizeAttribute action filter, which can be seen in any number of controller actions within CodeStash, an example of which is shown below

[Authorize]
[RenderTagCloud]
public ActionResult About()
{
    return View();
}

As we dicsussed earlier if there is no forms authentication token found and a controllers action is marked with the [Authorize] attribute, the user will be redirected to the login page, until such a time that they have logged in.

And this is what the Login view looks like, where the user can choose to login using

  • OpenId authentication, where they will not really need to enter anything just click their OpenId provider (providing they have logged in before and the forms authentication cookie is still around)
  • SStandard username/password (from registration process)

There are a couple of things to note there, such as:

  1. The browser's address bar shows a query string which includes a ReturnUrl which is set to ReturnUrl=/Site/Data, which just happens to be the controller/action URL of the page that we tried to load that required authorization before it could be viewed. This ReturnUrl query string parameter is a standard feature of Forms Authentication, which is what we will eventually use to store just the Authentication cookie.
  2. There are quite a few image buttons that can be clicked. Each of these images represents an OpenID compliant site that you could use to login with. For example, I have a Google account, so I may choose to use my Google credentials. I should point out that I got the bulk of the content for the Logon.aspx page from a blog somewhere, but I can not recall where from, so apologies for not mentioning the source directly in this article.

If I proceed to use my Google account by clicking on the Google image, the current browser session will be navigated to Google, where I can enter my normal login credentials, as shown below:

Here is the most relevant parts of the Login views markup

@model CodeStash.Models.Security.LogOnModel
@{    ViewBag.Title = "Log On";
}
@section SpecificPageHeadStuff
 {
    @Html.ScriptTag(Url.Content("~/Scripts/Controllers/Account/accountFunctions.js"))
    @Html.ScriptTag(Url.Content("~/Scripts/jquery.validate.min.js"))
    @Html.ScriptTag(Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js"))
}
@using CodeStash.ExtensionsMethods
<h2>
    Log On</h2>
<p>
    You may login by choosing your OpenId Provider, or by entering your CodeStash username
    and password. Or you can @Html.ActionLink("Register", "Register")
    if you don't have an account.
</p>
<form action="Authenticate?ReturnUrl=@HttpUtility.UrlEncode(Request.QueryString["ReturnUrl"])" 
        method="post" id="openid_form">
    <input type="hidden" name="action" value="verify" />
    <div class="logonBox">
        <fieldset>
            <legend>Login using OpenID</legend>
            <div class="openid_choice">
                <p>
                    Please click your account provider:</p>
                <div id="openid_btns">
                </div>
            </div>
            <div id="openid_input_area">
                @Html.TextBox("openid_identifier")
                <input type="submit" value="Log On" />
            </div>
            <noscript>
                <p>
                    OpenID is service that allows you to log-on to many different websites using a single
                    indentity. Find out <a href="http://openid.net/what/">more about OpenID</a> and
                    <a href="http://openid.net/get/">how to get an OpenID enabled account</a>.</p>
            </noscript>
            <div>
                @if (Model != null)
                {
                    if (String.IsNullOrEmpty(Model.UserName))
                    {
                    <div class="editor-label">
                        @Html.LabelFor(model => model.OpenID)
                    </div>
                    <div class="editor-field">
                        @Html.DisplayFor(model => model.OpenID)
                    </div>
                    <p class="button">
                        @Html.ActionLink("New User ,Register", "Register", new { OpenID = Model.OpenID })
                    </p>
                    }
                }
            </div>
        </fieldset>
    </div>
</form>
@Html.ValidationSummary(true, "Login was unsuccessful. Please correct the errors and try again.")
@using (Html.BeginForm("Logon", "Account", FormMethod.Post, new { id = "LogonForm" }))
{

    @Html.AntiForgeryToken("LogOn")
    
    <div class="logonBox">
        <fieldset>
            <legend>Or Login Normally</legend>
            <div class="editor-label">
                @Html.LabelFor(m => m.UserName)
            </div>
            <div class="editor-field">
                @Html.TextBoxFor(m => m.UserName, new { style = "width:300px" })
                @Html.ValidationMessageFor(m => m.UserName)
            </div>
            <div class="editor-label">
                @Html.LabelFor(m => m.Password)
            </div>
            <div class="editor-field">
                @Html.PasswordFor(m => m.Password, new { style = "width:300px" })
                @Html.ValidationMessageFor(m => m.Password)
            </div>
            <div class="editor-label">
                @Html.CheckBoxFor(m => m.RememberMe)
                <label for="RememberMe" class="centerAlignedText">
                    Remember me?</label>
            </div>
            <p>
                <br />
                <span class="btn"><a id="LogOnBtn" href="#">LogOn</a><span></span></span> <span class="clear">
                </span>
                <br />
            </p>
        </fieldset>
    </div>
}

One of the most important parts of the Login views markup is the actual form tag(s) markup. The Login view actually has 2 seperate form tags which deal with the different types of Login

OpenId Login Form Tag

This form ensures that the AccountController Authenticate action is called, which we saw in the AccountController's code above.

<form action="Authenticate?ReturnUrl=@HttpUtility.UrlEncode(Request.QueryString["ReturnUrl"])" 
        method="post" id="openid_form">
</form>

Standard Forms Authentication Login Form Tag

This form ensures that the AccountController Login POST action is called, which we saw in the AccountController's code above.

@using (Html.BeginForm("Logon", "Account", FormMethod.Post, new { id = "LogonForm" }))
{

}

Register

This is the page you would use to register a new user which would use standard forms authentication ie: username/password authentication

As forms authentication is so well known in ASP .NET development, I won't bore you with too many details I will just give you the bear bones details

So this is what the register portion of the AccountController looks like

using System;
using System.Web.Mvc;
using System.Web.Security;
using CodeStash.Models.Security;
using CodeStash.Services;
using DotNetOpenAuth.Messaging;
using DotNetOpenAuth.OpenId;
using DotNetOpenAuth.OpenId.Extensions.AttributeExchange;
using DotNetOpenAuth.OpenId.Extensions.SimpleRegistration;
using DotNetOpenAuth.OpenId.RelyingParty;

namespace CodeStash.Controllers
{
    public class AccountController : Controller
    {
        private static OpenIdRelyingParty openid = new OpenIdRelyingParty();
        private IFormsAuthenticationService formsService;
        private IMembershipService membershipService;
        private ILoggerService loggerService;

        public AccountController(   IFormsAuthenticationService formsService, 
                                    IMembershipService membershipService,
                                    ILoggerService loggerService)
        {
            this.formsService = formsService;
            this.membershipService = membershipService;
            this.loggerService = loggerService;
        }


        public ActionResult Register(string OpenID)
        {
            loggerService.Info("Register : New user registration selected");
            ViewBag.PasswordLength = membershipService.MinPasswordLength;
            ViewBag.OpenID = OpenID;
            return View();
        }

        [HttpPost]
        [ValidateAntiForgeryToken(Salt = "Register")]
        public ActionResult Register(RegisterModel model)
        {
            if (ModelState.IsValid)
            {
                // Attempt to register the user
                MembershipCreateStatus createStatus = membershipService.CreateUser(
			model.UserName, model.Password, model.Email, model.OpenID);

                if (createStatus == MembershipCreateStatus.Success)
                {
                    formsService.SignIn(model.UserName, false /* createPersistentCookie */);
                    loggerService.Info("Register : Sucess creating new user");
                    Session["EncryptedPasswordForUserToWriteDown"] = null;
                    return RedirectToAction("Index", "Home");
                }
                else
                {
                    ModelState.AddModelError("", AccountValidation.ErrorCodeToString(createStatus));
                }
            }
            else
            {
                loggerService.Info("Register : There were some error registering, " +
			"possibly due to missing/incorrect registration settings being supplied");
            }

            // If we got this far, something failed, redisplay form
            ViewBag.PasswordLength = membershipService.MinPasswordLength;
            return View(model);
        }
 
    }
}

Where we have the following Register view markup

@model CodeStash.Models.Security.RegisterModel

@{
    ViewBag.Title = "Register";
}
@section SpecificPageHeadStuff
 {
    @Html.ScriptTag(Url.Content("~/Scripts/Controllers/Account/accountFunctions.js"))
    @Html.ScriptTag(Url.Content("~/Scripts/jquery.validate.min.js"))
    @Html.ScriptTag(Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js"))

}
@using CodeStash.ExtensionsMethods

<h2>Create a New Account</h2>
<p>
    Use the form below to create a new account. 
</p>
<p>
    Passwords are required to be a minimum of @ViewBag.PasswordLength characters in length.
</p>

@using (Html.BeginForm("Register", "Account", FormMethod.Post, new { id = "RegisterForm" }))
{
    @Html.AntiForgeryToken("Register")
    @Html.ValidationSummary(true, 
        "Account creation was unsuccessful. Please correct the errors and try again.")
    <div>
        <fieldset>
            <legend>Account Information</legend>
            @if (ViewData["OpenID"] != null)
            {
            <div class="editor-label">
                @Html.Label("OpenID")
            </div>
            <div class="editor-label">
                @Html.Label((string)ViewBag.OpenID)
            </div>
            }
            <div class="editor-label">
                @Html.LabelFor(m => m.UserName)
            </div>
            <div class="editor-field">
                @Html.TextBoxFor(m => m.UserName, new { style = "width:300px" })
                @Html.ValidationMessageFor(m => m.UserName)
            </div>

            <div class="editor-label">
                @Html.LabelFor(m => m.Email)
            </div>
            <div class="editor-field">
                @Html.TextBoxFor(m => m.Email, new { style = "width:300px" })
                @Html.ValidationMessageFor(m => m.Email)
            </div>

            <div class="editor-label">
                @Html.LabelFor(m => m.Password)
            </div>
            <div class="editor-field">
                @Html.PasswordFor(m => m.Password, new { style = "width:300px" })
                @Html.ValidationMessageFor(m => m.Password)
            </div>

            <div class="editor-label">
                @Html.LabelFor(m => m.ConfirmPassword)
            </div>
            <div class="editor-field">
                @Html.PasswordFor(m => m.ConfirmPassword, new { style = "width:300px" })
                @Html.ValidationMessageFor(m => m.ConfirmPassword)
            </div>

            <p>
                <br />
                <span class="btn"><a id="RegisterBtn" 
			href="#">Register</a><span></span></span>
	            <span class="clear"></span>
			    <br />
            </p>

        </fieldset>
    </div>
}

And too be honest that this pretty much all there is to the register process, oh apart from this bit of Web.Config configuration that states that forms authentication is being used

<authentication mode="Forms">
  <forms loginUrl="~/Account/LogOn" timeout="2880" />
</authentication>

OpenId JavaScripts

There is also a set of javascripts for the OpenId selector, which is based on a freely available library which is downloadable from : http://code.google.com/p/openid-selector/

This will give you the following 4 components which are registered in the master page

  • openid-en.js
  • openid-jquery.js
  • openid-shadow.css
  • openid.css

Master Page

The master page (Views\Shared\_Layout.cshtml) provides all the common CSS/Javascript that CodeStash web site uses.

If you are not currently logged in you will see a master page like this

Once you are logged in you will see a master page like this

Shown below is the relevant markup for the Master page

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

@using Telerik.Web.Mvc.UI
@using CodeStash.ExtensionsMethods

<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>@ViewBag.Title</title>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />

    @Html.CssTag(Url.Content("~/Content/openid-shadow.css"))
    @Html.CssTag(Url.Content("~/Content/openid.css"))
    @Html.CssTag(Url.Content("~/Content/themes/base/jquery-ui.css"))
    @Html.CssTag(Url.Content("~/Content/themes/base/jquery.ui.dialog.css"))
    @Html.CssTag(Url.Content("~/Content/Highlighting/jquery.snippet.css"))
    @Html.CssTag(Url.Content("~/Content/site.css"))


    @Html.ScriptTag(Url.Content("~/Scripts/jquery-1.6.4.min.js"))
    @Html.ScriptTag(Url.Content("~/Scripts/modernizr-1.7.min.js"))
    @Html.ScriptTag(Url.Content("~/Scripts/jquery.tools.min.js"))
    @Html.ScriptTag(Url.Content("~/Scripts/jquery.tmpl.js"))
    @Html.ScriptTag(Url.Content("~/Scripts/openid-jquery.js"))
    @Html.ScriptTag(Url.Content("~/Scripts/openid-en.js"))
    @Html.ScriptTag(Url.Content("~/Scripts/jquery-ui-1.8.16.min.js"))
    @Html.ScriptTag(Url.Content("~/Scripts/Common/commonFunctions.js"))
    @Html.ScriptTag(Url.Content("~/Scripts/Highlighting/jquery.snippet.js"))
   
  
    @( Html.Telerik().StyleSheetRegistrar().DefaultGroup(group => group
        .DefaultPath("~/Content/Telerik")
        .Add("telerik.common.css")
        .Add("telerik.Black.min.css"))
    )
    
    <script type="text/javascript">
        $(document).ready(function () {
            openid.init('openid_identifier');
        });
    </script>
    @RenderSection("SpecificPageHeadStuff", false)
</head>
<body>
    <div id="header">
    </div>
    <div id="main-wrapper">
        <img id="logo" src="../../Content/Images/Logo.png" />
        <img id="logoFiles" src="../../Content/Images/files.png" />
        <div id="main">
            <div id="sidebar">
                <div class="gadget">
                    <h2>Settings</h2>
                    <div class="clr">
                    </div>
                    <ul class="sb_menu">
                        <li>@Html.Partial("_LogOnPartial")</li>
                        <li><a href="http://www.codeproject.com/Team">Team Settings</a></li>
                        <li><a href="http://www.codeproject.com/Account/ChangePassword">Change Password</a></li>
                        <li><a href="http://www.codeproject.com/Settings">Change Settings</a></li>
                    </ul>
                </div>
                <div class="gadget">
                    <h2>Actions</h2>
                    <div class="clr">
                    </div>
                    <ul class="sb_menu">
                        <li><a href="http://www.codeproject.com/Search/CreateSearch">Search</a></li>
                        <li><a href="http://www.codeproject.com/CodeSnippet">Add Code Snippet</a></li>
                        <li><a href="http://www.codeproject.com/CodeSnippet/OpenFromWeb">Open From Web</a></li>
                    </ul>
                </div>
                @Html.Partial("_TagCloudPartial")
            </div>
            <div id="mainbar">
                @RenderBody()
            </div>
            <div class="clr">
            </div>
        </div>
    </div>
</body>
    @(Html.Telerik().ScriptRegistrar()
            .Globalization(true)
            .jQuery(false).DefaultGroup(group => group
                .DefaultPath("~/Scripts/Telerik")
                .Add("telerik.common.min.js")
                .Add("telerik.grid.min.js")
                .Add("telerik.textbox.min.js")
                .Add("telerik.calendar.min.js")
                .Add("telerik.datepicker.min.js")
                .Add("telerik.grid.filtering.min.js"))
    )
</html>

There is not too much to say about this markup except that it utilises the hashing HtmlHelper extentions methods that we saw last time that hash the CSS/JavaScript files, and it also registeres the free Telerik MVC contributions controls which are used within Search, which we will see later.

Tag Cloud

The TagCloud is your typical arrangement of hyperlinks, where the cloud groups all the saved code snippets into their corresponding Categories (Category in the database), and will only show the top ranking sums of these snippets as a set of hyperlinks available on the Master Page (provided the user has logged in).

Here is what the TagCloud looks like. Obviously some of the categories shown below are just dummy ones I have added to illlustrate the point

The rendering of the tab cloud is largely done thanks to a specialized ActionFilter called RenderTagCloudAttribute which should ONLY be used on full page views. Though there is nothing to prevent it be used on controller actions that don't return full page views.

Here is the code from the RenderTagCloudAttribute

using System.Security.Principal;
using System.Web.Mvc;
using CodeStash.Controllers;

namespace CodeStash.Filters
{
    public class RenderTagCloudAttribute : ActionFilterAttribute
    {
        public override void OnActionExecuting(ActionExecutingContext filterContext)
        {
            IPrincipal user = ((Controller)filterContext.Controller).User;
      
            if (user != null && user.Identity.IsAuthenticated)
            {
                if (filterContext.Controller is BaseTagCloudEnabledController)
                {
                    ((BaseTagCloudEnabledController)filterContext.Controller).RenderAndCalculateTagCloud();
                }

                if (filterContext.Controller is BaseTagCloudEnabledAsyncController)
                {
                    ((BaseTagCloudEnabledAsyncController)filterContext.Controller).RenderAndCalculateTagCloud();
                }
            }
        }
    }
}

There is also a specialized BaseTagCloudEnabledController and BaseTagCloudEnabledAsyncController which can be inherited from that provide some base functionality for the Tag Cloud. Here is the relevant code

public abstract class BaseTagCloudEnabledAsyncController : AsyncController
{
    private readonly ITagCloudService tagCloudService;

    public BaseTagCloudEnabledAsyncController(ITagCloudService tagCloudService)
    {
        this.tagCloudService = tagCloudService;
    }


    public void RenderAndCalculateTagCloud()
    {
        Random rand = new Random();

        IEnumerable<TagCategoryModel> tags = this.tagCloudService.CreateTagCloud();
        if (tags.Any())
        {
            if (tags.Count() < 10)
                ViewData["TagCloud"] = tags;
            else
                ViewData["TagCloud"] = tags.Take(10);
        }
        else
            ViewData["TagCloud"] = new List<TagCategoryModel>();

        ViewData["Rand"] = rand;

    }
}

It can be seen that this code makes use of a tagCloudService which works as follows:

using System.Collections.Generic;
using System.Linq;
using CodeStash.Common.DataAccess.EntityFramework;
using CodeStash.Common.DataAccess.Repository;
using CodeStash.Common.DataAccess.UnitOfWork;
using CodeStash.Models.TagCloud;

namespace CodeStash.Services
{
    public class TagCloudService : ITagCloudService
    {
        private readonly IUnitOfWork unitOfWork;
        private readonly IRepository<CodeSnippet> codeSnippetRepository;
        private readonly IRepository<CodeCategory> codeCategoryRepository;


        public TagCloudService(IUnitOfWork unitOfWork,
                              IRepository<CodeSnippet> codeSnippetRepository,
                              IRepository<CodeCategory> codeCategoryRepository)
        {
            this.unitOfWork = unitOfWork;
            this.codeSnippetRepository = codeSnippetRepository;
            this.codeCategoryRepository = codeCategoryRepository;
        }


        public IEnumerable<TagCategoryModel> CreateTagCloud()
        {

            using (unitOfWork)
            {
                codeSnippetRepository.EnrolInUnitOfWork(unitOfWork);
                codeCategoryRepository.EnrolInUnitOfWork(unitOfWork);

                int totalCodeSnippets = codeSnippetRepository.FindAll().Count();

                var categories = codeCategoryRepository.FindAll("CodeSnippets").AsEnumerable();
                var tagCategories = 
                    (from c in categories
                    orderby c.CodeCategoryName
                    select new TagCategoryModel
                    {
                        CategoryId = c.CodeCategoryId,
                        CategoryName = string.Format("{0}", c.CodeCategoryName.Trim()),
                        CountOfCategory = c.CodeSnippets.Count(),
                        TotalArticles = totalCodeSnippets
                    });

                return (from x in tagCategories
                        where x.CountOfCategory > 0
                        orderby x.CountOfCategory descending
                        select x).ToList();
            }
        }
    }
}

The master page _Layout.cshtml has this line @Html.Partial("_TagCloudPartial") in it which renders the Tag Cloud partial view which is shown below

@if (ViewData["TagCloud"] != null)
{
    IEnumerable<CodeStash.Models.TagCloud.TagCategoryModel> cats =
        (IEnumerable<CodeStash.Models.TagCloud.TagCategoryModel>)ViewData["TagCloud"];

    if (cats.Any())
    {
        <div class="gadget">
            <h2>Tag Cloud</h2>
            <div class="clr">
            </div>
            <div id="tagCloud">
            
                @foreach (var t in cats)
                {
                    Random rand = (Random)ViewData["Rand"];
                    string[] colors = new string[] { "#21587D", "#3181B7", 
				"#1273B5", "#0D5382", "#2C5E7F", "#347096" };

                    <span>
                        @Html.ActionLink(string.Format("{0} ",t.CategoryName),
				"DisplaySnippetsForCategory","CodeSnippet",
                        new { category= t.CategoryName},
                        new 
                        { 
                            style=string.Format("color : {0}",colors[rand.Next(colors.Length)]),
                            @class = CodeStash.Utils.WebSiteUtils.GetTagClass(t.CountOfCategory, t.TotalArticles)
                        })
                    </span>
                }
            </div>
    </div>
    }
}

It can be seen that this view simple renders the data that is stored in the ViewData

Profile Settings

The profile page allows a logged in CodeStash web site user to adjust their personal settings. At the moment this is limited to 2 settings but this may expand in the future

  • Maximum snippets to display : This settings limits the displaying of snippets to a maximum number of items. If this number is exceeded a better search should be conducted
  • Snippet highlighting CSS : This picks what CSS styles to use for the snippet highlighting
public class SettingsController : BaseTagCloudEnabledController
{
    private readonly ILoggerService loggerService;
    private readonly IMembershipService membershipService;




    public SettingsController(
        ILoggerService loggerService, 
        IMembershipService membershipService,
        ITagCloudService tagCloudService)
        : base(tagCloudService)
    {
        this.loggerService = loggerService;
        this.membershipService = membershipService;
    }


    [Authorize]
    [RenderTagCloud]
    [HttpGet]
    public ActionResult Index()
    {

        if (User.Identity.IsAuthenticated)
        {
            MembershipUser user = 
		membershipService.GetUserByUserName(User.Identity.Name);
            UserSettingsProfileModel profile = 
		UserSettingsProfileModel.GetUserProfile(User.Identity.Name);
            ChangeSettingsModel vm = new ChangeSettingsModel();
            ....
            ....
            return View(vm);
        }
        else
        {
            return RedirectToAction("Index", "Home");
        }

    }


    [Authorize]
    [HttpPost]
    [AjaxOnly]
    [ValidateAntiForgeryToken(Salt = "ChangeSettings")]
    public ActionResult ChangeSettings(ChangeSettingsModel vm)
    {
        try
        {
            if (ModelState.IsValid)
            {
                MembershipUser user = 
			membershipService.GetUserByUserName(User.Identity.Name);
                UserSettingsProfileModel profile = 
			UserSettingsProfileModel.GetUserProfile(User.Identity.Name);
                ....
                ....
                profile.Save();
                ....
                ....
            }
            else
            {
                ViewData["successfulEdit"] = false;
                ....
                ....
            }
        }
        catch
        {
            ....
            ....
            ViewData["successfulEdit"] = false;
        }
    }
}

It can seen that the SettingsController simply renders a default view and also allows the updating of the custom UserSettingsProfileModel which is as follows

public class UserSettingsProfileModel : ProfileBase
{
    [SettingsAllowAnonymous(false)]
    public bool IsOpenIdLoggedInUser
    {
        get { return (bool)base["IsOpenIdLoggedInUser"]; }
        set { base["IsOpenIdLoggedInUser"] = value; }
    }

    [SettingsAllowAnonymous(false)]
    public int HighlightingCSSId
    {
        get { return (int)base["HighlightingCSSId"]; }
        set { base["HighlightingCSSId"] = value; }
    }

    [SettingsAllowAnonymous(false)]
    public int MaxSnippetsToDisplay
    {
        get { return (int)base["MaxSnippetsToDisplay"]; }
        set { base["MaxSnippetsToDisplay"] = value; }
    }

    public static UserSettingsProfileModel GetUserProfile(string username)
    {
        return Create(username) as UserSettingsProfileModel;
    }

    public static UserSettingsProfileModel GetUserProfile()
    {
        return Create(Membership.GetUser().UserName) as UserSettingsProfileModel;
    }
}

It can also be seen in the SettingsController code that it makes use of a IMembershipService class. That is simply a helper class that implements the following interface, and deals with communicating with the standard ASP .NET Membership database

public interface IMembershipService
{
    int MinPasswordLength { get; }
    bool ValidateUser(string userName, string password);
    MembershipCreateStatus CreateUser(string userName, string password, string email, string OpenID);
    Tuple<MembershipCreateStatus, String> CreateUser(string userName, string email, string OpenID);
    bool ChangePassword(string userName, string oldPassword, string newPassword);
    MembershipUser GetUserByOpenId(string OpenID);
    MembershipUser GetUserByUserName(string UserName);
}

Ok so now lets move on to look at the pages markup, which is pretty simple and the most important parts look like this

@model CodeStash.Models.Settings.ChangeSettingsModel
@{
	ViewBag.Title = "Change Settings";
	Layout = "~/Views/Shared/_Layout.cshtml";
}
@using CodeStash.ExtensionsMethods
@section SpecificPageHeadStuff
 {
     @Html.CssTag(Url.Content("~/Content/Controllers/Settings/settings.css"))
     @Html.ScriptTag(Url.Content("~/Scripts/Controllers/Settings/settings.js"))
}


<div id="dialog-message"  style="display:none;">
	<p id="dialog-message-content">
	</p>
</div>

<div id="ChangeSettingsPanel">
    @Html.Partial("ChangeSettingsPartial",Model)
</div>

There is basically a Partial view that is rendered that shows all the markup for the actual settinsgs for the user. You can probably imagine that markup from the settings screen shot above.

So lets just look at the JavaScript side of things now shall we. The settings JavaScript is shown in its entirety below

$(document).ready(function () {

    InitBinding();

});



function InitBinding() {

    console.log($('#successfulEdit').val());

	//On submit on add page, submit the add and shows the success
	//page it the edit was successful, otherwise add page is shown again including
	//validation errors
    $("#Submit").click(function (e) {
        e.preventDefault();
        CallPostRequestForChangeSetting();
    });
}



//Ajax request to load in next page of results
function CallPostRequestForChangeSetting() {

 
    var formData = $("#ChangeSettingsForm").serialize();

    $.post("/Settings/ChangeSettings", formData, function (response) {
        $("#AjaxSettingsContents").replaceWith(response);
        InitBinding();

        var successfulAdd = $('#successfulEdit').val();
        if (successfulAdd == "True") {
            showOkDialog('Sucessfully saved your settings', 180, 'Information');
        }
        else {
            showOkDialog('Could not update your user settings', 180, 'Error');
        }
    });
	return false;
}

 

Team Settings

This page allows logged in users to create teams. The basic idea is that the first user to create a team, will be the team owner, whom can then alter the team, by adding/removing team members

If we look at the teams related schema entries it should not be that hard to see how this screen hangs together

The page basically offers these steps to allow a new team of users to be created.

Creating A New Team And Give It A Name

All you need to do here is type a new team name and click the "Create Team" button which will create a brand new team which you can then assign team members to. Alternatively you may pick existing teams that you are the owner of, these are shown in the select just below the textbox where you can type a new team name.

Searching For Users To Add To Team

Once you have either created a new team, or picked an existing one you want to add team members to, you must search for team members, this is done using the section of the team settings page as shown below.

  • Where you MUST enter the team members email
  • Where you can choose whether that team members login is OpenId or not. This will help find the exact user for the team

So it starts by entering the team members email, and then hitting the "Search For Users" button, after that some more of the team settings page is revealed which shows the matched user(s) with that email. These are shown in a select list, from which you can pick them and click the "Assign User To Team" button.

Manipulating The Team Members

Once you have added members to the selected (or new) team, you will see them all presented in the bottom area of the team settings page. If you decide you want to remove a particular member from a team, you can either

  • Click the red cross at the top of that particular team member
  • Drag and drop the team member to the trash can shown (if you let go when its not on the trash can, it will fly back into its old position)

Here is the most relevant parts of the team pages markup

@model CodeStash.Models.Team.TeamModel
@{
	ViewBag.Title = "Team Settings";
	Layout = "~/Views/Shared/_Layout.cshtml";
}
@section SpecificPageHeadStuff
 {
	 @Html.CssTag(Url.Content("~/Content/Controllers/Team/team.css"))
	 @Html.ScriptTag(Url.Content("~/Scripts/Controllers/Team/team.js"))
}
@using CodeStash.ExtensionsMethods


<div id="dialog-message"  style="display:none;">
	<p id="dialog-message-content">
	</p>
</div>

<input type="hidden" id="CurrentDraggablePosition" />

<div id="TeamPanel">

	
	<div class="headedPanel">
		<div class="stepPanel">
			<br />
			<h3>
				Step 1 : Create New Team, Or Pick Existing Team</h3>
			<p>
				<strong>Create A New Team</strong>
				<br />
				@Html.TextBoxFor(x => x.TeamName)
				<span class="btn"><a id="CreateTeam">Create Team</a><span></span></span>
				...
				...
				...
				<select id="OwnedTeams" style="display: none" class="selectBox">
				</select>
			</p>
		</div>
	</div>
	<div id="AssignToTeamPanel"  class="headedPanel" style="display:none">
		<div class="stepPanel">
			<br />
			<h3>
				Step 2 : Pick Your Team Members, And Assign To Selected Team</h3>
			<p>
				<strong>Search For User To Add To Selected Team</strong>
				<br />
				@Html.TextBoxFor(x => x.Email)
				<span class="btn"><a id="SearchForUsers">Search For Users</a><span></span></span>
				...
				...
				...
				<input id="IsOpenIdLogin" type="checkbox" />
				@Html.LabelFor(x => x.IsOpenIdLogin, "Search For OpenId Registered Users")
				...
				...
				...
				<div id="FoundUsersPanel" style="display: none">
					<strong>Users That Matched Your Search</strong>
					...
					...
					...

					<select id="FoundUsers" class="selectBox">
					</select>
					<div style="position: absolute;margin-top: -30px;margin-left: 205px;width: 170px;">
					<span class="btn"><a id="AssignUser">Assign User To Team</a><span></span></span>
					...
					...
					...
				</div>
			</p>
		</div>
	</div>
	<div id="TeamMembersPanel" class="headedPanel" style="display:none">
		<div class="stepPanel">
			<br />
			<h3>Step 3 : Remove People From The Selected Team (If You Must)</h3>
			<br />

			<div id="teamMemberContainer"></div>


			<script id="teamMemberTemplate" type="text/x-jQuery-tmpl">
				<div class="ui-widget-content draggable">
					<input type="hidden" class="draggableHidden" value="${UserId}" />
					<span class="username">${UserName}</span>
					<img src="../../Content/images/people.png" class="TeamPeopleIcon" />
					<img src="../../Content/images/delete.png" class="TeamDeleteIcon" />
					<div>
						<a href="mailto:${Email}">${UserName}</a>
					</div>
				</div>
				<div class="tooltip">
					You can drag ${UserName} to the bin to remove them from the current team
				</div>
			</script>

			<div class="ui-widget-header droppable">
			</div>
		</div>
	</div>
</div>

It can be seen that the bottom portion of the screen makes use of the cool jQuery Template idea. Which I like a lot

Ok so lets continue on to look at the most relevant parts of the team pages JavaScript

$(document).ready(function () {

    GetOwnedTeams();

    $('.TeamDeleteIcon').live('click', function () {
        var id = $(this).parent().find('.draggableHidden').val();
        DeleteMemberFromTeam(id, undefined);
    });



    $('#CreateTeam').click(function () {

        var newTeamName = $('#TeamName').val();
        if (newTeamName == undefined || newTeamName == '') {
            $('#TeamName').addClass('Error');
        }
        else {
            $('#TeamName').removeClass('Error');
            $.post("/Team/SaveNewTeam", { teamName: newTeamName },
            function (data) {
                	....
                	....
                	....

            },
            "json");
        }
    });


    $(".droppable").droppable({
        hoverClass: "ui-state-active",
        drop: function (event, ui) {
            $(this).addClass("ui-state-highlight");

            var fullDragUI = $(ui.draggable.context);
            var id = fullDragUI.find('.draggableHidden').val();
            DeleteMemberFromTeam(id, ui.draggable);
        }
    });


    $('#AssignUser').click(function () {
        $.post("/Team/AssignMemberToTeam", { teamId: $('#OwnedTeams').val(), teamMemberId: $('#FoundUsers').val() },
        function (data) {
            if (data.Success != undefined && data.Success) {
                GetTeamMembersForSpecificTeam($('#OwnedTeams').val());
            }
            else {
                showOkDialog(data.Message, 180, 'Error');
            }
        },
        "json");
    });



    $('#OwnedTeams').change(function () {
        GetTeamMembersForSpecificTeam($('#OwnedTeams').val());
    });



    $('#SearchForUsers').click(function () {

        var emailToSearchFor = $('#Email').val();

        if (emailToSearchFor == undefined || emailToSearchFor == '') {
            $('#Email').addClass('Error');
        }
        else {
            $.post("/Team/SearchForUsers", { email: $('#Email').val(), isOpenIdlogin: $('#IsOpenIdLogin').is(':checked') },
                function (data) {
                    if (data.Success != undefined && data.Success) {
                 	....
                	....
                	....

                    }
                    else {
                        showOkDialog(data.Message, 180, 'Error');
                    }
                },
                "json");
        }
    });
});



function DeleteMemberFromTeam(userId, draggable) {

    showYesNoDialog('Are you sure you want to delete this user from the selected team?', 180, 'Confirm',
                function () {
                    $.post("/Team/DeleteMemberFromTeam", { teamId: $('#OwnedTeams').val(), teamMemberId: userId },
                	....
                	....
                	....
                        },
                        "json");
                },
                function () {
                    //animate draggable back currentDraggablePosition
                    AnimateDraggableBack(draggable);

                }
            );
}


function GetTeamMembersForSpecificTeam(teamId) {

    $("#teamMemberContainer").empty();

    $.post("/Team/GetTeamMembersForSpecificTeam", { teamId: teamId },
            function (data) {
                if (data.Success != undefined && data.Success) {
                    if (data.Message.length > 0) {
                        $("#teamMemberTemplate").tmpl(data.Message).appendTo("#teamMemberContainer");
                        $("#TeamMembersPanel").show();
                    }
                    else {
                        $("#TeamMembersPanel").hide();
                    }
                }
                else {
                    $("#TeamMembersPanel").hide();
                    showOkDialog(data.Message, 180, 'Error');
                }


                $(".draggable").tooltip({ effect: 'slide' });

                $(".draggable").draggable({

                    revert: 'invalid',
                    stop: function () {
                        $(this).draggable('option', 'revert', 'invalid');
                    },
                    start: function (event, ui) {
                        $('#CurrentDraggablePosition').val(ui.position);
                    },
                    drag: function (event, ui) {
                        $(".draggable").each(function () {
                            $(this).tooltip().hide();
                        });
                    }
                });

            },
            "json");
}




function GetOwnedTeams() {

    $.post("/Team/GetOwnedTeams",
            function (data) {
                if (data.Success != undefined && data.Success) {
                ....
                ....
                ....
                }
                else {
                    $('#OwnedTeams').hide();
                    $('#AssignToTeamPanel').hide();
                }
            },
            "json");
}



function AnimateDraggableBack(element) {

    if (element != undefined) {
        //animate draggable back currentDraggablePosition
        element.animate({
            left: $('#CurrentDraggablePosition').val(value).left,
            top: $('#CurrentDraggablePosition').val(value).top
        }, 600, "easeOutElastic");
    }

}

I left the drag and drop stuff in there as that is some of the more interesting jQuery that makes use of the excellent jQuery UI library, which is freekin ace.

And now finally lets look at the main controller methods, which I hope you can see being called from the JavaScript above

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.Mvc;
using System.Web.Security;
using CodeStash.Common.DataAccess.EntityFramework;
using CodeStash.Common.DataAccess.Repository;
using CodeStash.Common.DataAccess.UnitOfWork;
using CodeStash.Common.Encryption;
using CodeStash.Filters;
using CodeStash.Models.Security;
using CodeStash.Services;
using CodeStash.Services.Contracts;




namespace CodeStash.Controllers
{
    public class TeamController : BaseTagCloudEnabledController
    {

        private readonly IMembershipService membershipService;
        private readonly IMembershipDataProvider membershipDataProvider;
        private readonly ILoggerService loggerService;
        private readonly IRepository<OwnedTeam> ownedTeamRepository;
        private readonly IRepository<CreatedTeam> createdTeamRepository;
        private readonly IUnitOfWork unitOfWork;



        public TeamController(IMembershipService membershipService,
            IMembershipDataProvider membershipDataProvider,
            ILoggerService loggerService, 
            ITagCloudService tagCloudService,
            IRepository<OwnedTeam> ownedTeamRepository, 
            IRepository<CreatedTeam> createdTeamRepository,
            IUnitOfWork unitOfWork)
            : base(tagCloudService)
        {
	....
	....
	....
        }



        [Authorize]
        [RenderTagCloud]
        [HttpGet]
        public ActionResult Index()
        {
            return View();
        }


        [Authorize]
        [HttpPost]
        [AjaxOnly]
        public ActionResult SaveNewTeam(string teamName)
        {
	....
	....
	....

        }


        [Authorize]
        [HttpPost]
        [AjaxOnly]
        public ActionResult GetOwnedTeams()
        {
	....
	....
	....
        }



        [Authorize]
        [HttpPost]
        [AjaxOnly]
        public ActionResult GetTeamMembersForSpecificTeam(int teamId)
        {
	....
	....
	....
        }



        [Authorize]
        [HttpPost]
        [AjaxOnly]
        public ActionResult AssignMemberToTeam(int teamId, string teamMemberId)
        {
	....
	....
	....
        }



        [Authorize]
        [HttpPost]
        [AjaxOnly]
        public ActionResult DeleteMemberFromTeam(int teamId, string teamMemberId)
        {
	....
	....
	....

        }



        [Authorize]
        [HttpPost]
        [AjaxOnly]
        public ActionResult SearchForUsers(string email, bool isOpenIdlogin)
        {
	....
	....
	....

        }
    }
}

Open From Web

This page allows you to enter a url to an existing web based snippet and have it highlighted. Essentially all that is required is that the source is parsed and a code snippet is extracted and highlighted if possible.

Here is the most relevant code from the CodeSnippetController

[Authorize]
[RenderTagCloud]
[HttpGet]
public ActionResult OpenFromWeb()
{
    OpenFromWebViewModel vm = new OpenFromWebViewModel();
    vm.HighlightingCSS = CodeSnippetUtils.GetUsersSavedHighlightingCSS(User.Identity);
    AddLanguagesToOpenFromWebVm(vm);
    return View(vm);
}


[Authorize]
[RenderTagCloud]
[HttpPost]
[AjaxOnly]
[ValidateAntiForgeryToken(Salt = "OpenFromWeb")]
public ActionResult OpenFromWeb(OpenFromWebViewModel vm)
{
    try
    {
        OpenFromWebViewModel newVm = new OpenFromWebViewModel();
        newVm.HighlightingCSS = CodeSnippetUtils.GetUsersSavedHighlightingCSS(User.Identity);
        newVm.ActualCode = CodeSnippetUtils.ReadContentFromWebUrl(vm.FileName);
        AddLanguagesToOpenFromWebVm(newVm);
        newVm.LanguageId = vm.LanguageId;
        newVm.CodeHasBeenParsed = ModelState.IsValid && !string.IsNullOrWhiteSpace(newVm.ActualCode);
        return PartialView("OpenFromWebPartial", newVm);
    }
    catch(Exception ex)
    {
        OpenFromWebViewModel newVm = new OpenFromWebViewModel();
        newVm.HighlightingCSS = CodeSnippetUtils.GetUsersSavedHighlightingCSS(User.Identity);
        AddLanguagesToOpenFromWebVm(newVm);
        newVm.CodeHasBeenParsed = false;
        return PartialView("OpenFromWebPartial", newVm);
    }
}

Where this code make use of this utility code

public static string ReadContentFromWebUrl(string url)
{
    try
    {
        System.Net.HttpWebRequest fr = (System.Net.HttpWebRequest)System.Net.HttpWebRequest.Create(new Uri(url));
        if ((fr.GetResponse().ContentLength > 0))
        {
            System.IO.StreamReader str = new System.IO.StreamReader(fr.GetResponse().GetResponseStream());
            return str.ReadToEnd();
        }
        return "";
    }
    catch (System.Net.WebException ex)
    {
        return "";
    } 
}

The most relevant parts of the markup are shown below

@model CodeStash.Models.Snippet.OpenFromWebViewModel
@{
	ViewBag.Title = "Open From Web";
	Layout = "~/Views/Shared/_Layout.cshtml";
}

@using CodeStash.ExtensionsMethods


@section SpecificPageHeadStuff
 {
     @Html.CssTag(Url.Content("~/Content/Controllers/CodeSnippet/openFromWeb.css"))
     @Html.ScriptTag(Url.Content("~/Scripts/Controllers/CodeSnippet/openFromWeb.js"))
}



<div id="dialog-message"  style="display:none;">
	<p id="dialog-message-content">
	</p>
</div>

<div id="AddSnippetPanel">
    
    <h2>Open From Web</h2>
    @Html.Partial("OpenFromWebPartial",Model)

</div>

And here is the relevant JavaScript

$(document).ready(function () {

	InitBinding();
});



function InitBinding() {

 
	//On submit on add page, submit the add and shows the success
	//page it the edit was successful, otherwise add page is shown again including
	//validation errors
    $("#Submit").click(function (e) {
        e.preventDefault();
        CallPostRequestForOpen();
    });
}



//Ajax request to load in next page of results
function CallPostRequestForOpen() {

    var formData = $("#OpenFromWebForm").serialize();

    $.post("/CodeSnippet/OpenFromWeb", formData, function (response) {
        $("#AjaxContents").replaceWith(response);
	    InitBinding();

	    var successfulParse = $('#successfulParse').val();
	    if (successfulParse == "True") {
	        showOkDialog('Your parsed snippet is shown below', 180, 'Error');

	        SwitchHightlighting($("#HighlightingCSSToUse").val());

	    }
	    else {
	        showOkDialog('Could not parse your code snippet', 180, 'Error');
	    }
	});
	return false;
}

Add Snippet

The add snippet is basically just a simple INSERT statement at the end of the day. It looks like this:

This page allow logged in users to create new code snippet

The view looks like this

@model CodeStash.Models.Snippet.AddSnippetViewModel
@using CodeStash.ExtensionsMethods

<div id="AjaxAddContents">
    @using (Html.BeginForm("Add", "CodeSnippet", FormMethod.Post, new { id = "AddForm" }))
    {
        @Html.AntiForgeryToken("Add")
        
        <h2>Add A New Code Snippet</h2>
        <p>Please fill in the details below, and when ready click the "Add" button at the bottom of the page</p>


        
        <input id="successfulAdd" type="hidden" value="@ViewData["successfulAdd"]" />
        <input id="addedSnippetId" type="hidden" value="@ViewData["addedSnippetId"]" />
        
        <div class="headedPanel">
            <strong>Title</strong>
            <div>
                @Html.TextBoxFor((x) => x.Title)
                 
                @Html.ValidationMessageFor((x) => x.Title)
            </div>
        </div>
        
        <div class="headedPanel">
            <strong>Description</strong>
            <div>
                @Html.TextBoxFor((x) => x.Description)
                 
                @Html.ValidationMessageFor((x) => x.Description)
            </div>
        </div>
        
        <div class="headedPanel">
            <strong>Category</strong>
            <div>
                Search for existing category
                <br />
                <input id="SearchCategoryText" type="text" />
                <span class="btn"><a id="SearchForExistingCategories">Search</a><span></span></span>
                <span class="clear"></span>
                <select id="FoundCategories" style="display: none" class="selectBox">
                </select>
                <br />
                Or fill in new category
                <br />
                @Html.TextBoxFor((x) => x.NewCodeCategoryName)
                 
                @Html.ValidationMessageFor((x) => x.NewCodeCategoryName)
            </div>
        </div> 
   
        
        <div class="headedPanel">
            <strong>Grouping</strong>
            <div>
                Search for existing grouping
                <br />
                <input id="SearchGroupingText" type="text" />
                <span class="btn"><a id="SearchForExistingGrouping">Search</a><span></span></span>
                <span class="clear"></span>
                <select id="FoundGrouping"  style="display: none" class="selectBox">
                </select>
                <br />
                Or fill in new grouping
                <br />
                @Html.TextBoxFor((x) => x.NewGroupingName)
                 
                @Html.ValidationMessageFor((x) => x.NewGroupingName)
            </div>
        </div>
        
        <div class="headedPanel">
            <strong>Language</strong>
            <div>
                @Html.ComboFor(x => x.LanguageId, 
                x => x.LanguageList, 
                x => x.LanguageId, 
                x => x.Language1, 
                    new Dictionary<string,object> { 
                        { "style", "width:400px"}, 
                        { "class", "selectBox"  }})
                 
                @Html.ValidationMessageFor((x) => x.LanguageId)
            </div>
        </div>
        
        <div class="headedPanel">
            <strong>Code Snippet Visibility</strong>
            <div>
                    @Html.ComboFor(x => x.Visibility, 
                    x => x.VisibilityList, 
                    x => x.Id, 
                    x => x.VisibilityDescription,
                    new Dictionary<string, object> { 
                        { "style", "width:400px"}, 
                        { "class", "selectBox"  }})
                 
                @Html.ValidationMessageFor((x) => x.Visibility)
            </div>
        </div>
        
        <div class="headedPanel">
            <div>
                <strong>Tags</strong>     Enter tags seperated by ";"
                <div>
                    @Html.TextBoxFor((x) => x.Tags)
                     
                    @Html.ValidationMessageFor((x) => x.Tags)
                </div>
            </div>
        </div>
        
        <div class="headedPanel">
            <strong>Actual Code</strong>
            <div>
                @Html.TextAreaFor((x) => x.ActualCode, new { id = "editTextFieldsCode" })
                 
                <div id="actualCodeError">@Html.ValidationMessageFor((x) => x.ActualCode)</div>
            </div>
        </div>
        
        <span class="btn"><a id="AddSubmit">Add</a><span></span></span>
        <span class="clear"></span>
        <br />
        <br />        
                
        
    }
</div>

And the most relevant part of the JavaScript looks like this, where the form gets submitted to the CodeSnippet controller's Add action

//Ajax request to load in next page of results
function CallPostRequestForAddConfirmSnippet() {

	var formData = $("#AddForm").serialize();

	$.post("/CodeSnippet/Add", formData, function (response) {
	    $("#AjaxAddContents").replaceWith(response);
	    InitBinding();

	    var successfulAdd = $('#successfulAdd').val();
	    if (successfulAdd == "True") {
	        var addedSnippetId = $('#addedSnippetId').val();
	        window.location.href = '/CodeSnippet/DisplaySnippetsForAddAndEdit' 
            + '?codeSnippetId=' + addedSnippetId;
	    }
	    else {
	        showOkDialog('Could not save your code snippet', 180, 'Error');
	    }
	});
	return false;
}

Where the CodeSnippet controller's Add action looks like this

[Authorize]
[HttpPost]
[ValidateInput(false)] // allow through code type text for this Action
[AjaxOnly]
[ValidateAntiForgeryToken(Salt = "Add")]
public ActionResult Add(AddSnippetViewModel vm)
{
    try
    {
        if (ModelState.IsValid)
        {
            CodeSnippet addedSnippet = AddOrUpdateSnippet(vm, false);
            ViewData["successfulAdd"] = true;
            ViewData["addedSnippetId"] = addedSnippet.CodeSnippetId;
            RefreshAddModelStaticData(vm);
            return PartialView("AddSnippetPartial", vm);
        }
        else
        {
            ViewData["successfulAdd"] = false;
            ViewData["addedSnippetId"] = 0;
            RefreshAddModelStaticData(vm);
            return PartialView("AddSnippetPartial", vm);
        }

    }
    catch
    {
        RefreshAddModelStaticData(vm);
        ViewData["successfulAdd"] = false;
        return PartialView("AddSnippetPartial", vm);
    }
}

The only other thing to note is that the System.DataAnnotations namespace is used to allow the validation of the form to occur. In fact DataAnnotations are used throughout CodeStash

Here is an example of how these look

public class AddSnippetViewModel : ISnippetViewModel
{
    #region Ctor
    /// <summary>
    /// For Post request where ModelBinding takes care of matching up properties for us
    /// </summary>
    public AddSnippetViewModel()
    {
        LanguageList = new List<Language>();
        VisibilityList = new List<Visibility>();

    }
    #endregion

    #region Public Properties

    public List<Language> LanguageList { get; set; }

    [Required(ErrorMessage = "You must enter a value for Language")]
    public int LanguageId { get; set; }

    public int CodeCategoryId { get; set; }


    [Required(ErrorMessage = "You must enter a value for Category")]
    public string NewCodeCategoryName { get; set; }


    public int GroupId { get; set; }


    [StringLength(100, MinimumLength = 0,
        ErrorMessage = "Grouping must be between 0-100 characters in length")]
    public string NewGroupingName { get; set; }


    [StringLength(100, MinimumLength=0,
        ErrorMessage = "Tags must be between 0-100 characters in length")]
    public string Tags { get; set; }


    [Required(ErrorMessage = "You must enter a value for Description")]
    public string Description { get; set; }


    [Required(ErrorMessage = "You must enter a value for Title")]
    public string Title { get; set; }


    [Required(ErrorMessage = "You must enter a value for ActualCode")]
    public string ActualCode { get; set; }


    public List<Visibility> VisibilityList { get; set; }

    [Required(ErrorMessage = "You must enter a value for Visibility")]
    public int Visibility { get; set; }
        

    public Guid AspNetMembershipUserId { get; set; }

    #endregion
}

When a snippet is successfullt added it will be displayed, along with any other snippets in its group (if it was created in a group)

Search Snippet

Seaching for snippets is obviously one of the core features of CodeStash, and as I have already stated CodeStash supports numerous methods of searching for snippets such as

  • By Tag
  • By Keyword
  • By Language

All of these can be further limited by a visibility modifier. The search screen is as shown below:

It can be seen that this page simply allows you to set up your desired search. I will dive into the nuts and bolts of how search works  in a  minute, but for now let us just look at another screen shot or 2.

When you click the button that starts a asychronous search, which causes a progress wheel to be shown

When the search finishes, the results page is shown which shows a DataGrid (or a message stating "no results could be found")

It can be seen (remember you can click these images to see a bigger version) that there is a DataGrid of results (I am using the TTelerik ASP MVC Extensions DataGrid (which is free)) which has 2 columns with hyperlinks in it, these are as follows:

  • Popup : Will show a popup dialog which shows the snippet in it
  • Show : Will actually display the snippets using the Display Snippets page which is shown below

Here is an example of what will be shown when you click one a hyperlink in the "Popup" column of the grid

If the user clicks the "Show" hyperlink they are directed to the Display Snippets page,which will show the selected snippet and any other snippet that is part of that group (if the selected snippet for viewing is actually part of a group)

So that's the screen shots of how "Search" hangs together so lets now get into the guts of it

I think the first thing we should start with is how the search page allows users to search, which is a pretty simple form that uses the following markup

@model CodeStash.Models.Search.CreateSearchViewModel
@using CodeStash.ExtensionsMethods
@{
	ViewBag.Title = "Create Search";
	Layout = "~/Views/Shared/_Layout.cshtml";
}

@section SpecificPageHeadStuff
 {
     @Html.CssTag(Url.Content("~/Content/Controllers/Search/search.css"))
     @Html.ScriptTag(Url.Content("~/Scripts/Controllers/Search/search.js"))
}


<div id="dialog-message"  style="display:none;">
	<p id="dialog-message-content">
	</p>
</div>


<div id="SearchPanel">
 @using (Html.BeginForm("CreateSearch", "Search", FormMethod.Post, new { id = "CreateSearchForm" }))
    {
     
        @Html.AntiForgeryToken("CreateSearch")
        
        <div id="allFormData">
        
            <input id="successfulCreate" type="hidden" value="@ViewData["successfulCreate"]" />

        
 	        <h2>Create Your Search</h2>
	        <p>You can create your search by choosing and filling one of the sections below, and then cliking the
            submit button, where you will be redirected to the search results.</p>
        

            <div class="headedPanel">
		        <div class="stepPanel">

                    <label for="searchType_ByTag">ByTag</label>
                    @Html.RadioButtonFor(x => x.SearchType, "ByTag", new { id = "searchType_ByTag" })

                    <label for="searchType_ByKeyWord">ByKeyWord</label>
                    @Html.RadioButtonFor(x => x.SearchType, "ByKeyWord", new { id = "searchType_ByKeyWord" })

                    <label for="searchType_ByLanguage">ByLanguage</label>
                    @Html.RadioButtonFor(x => x.SearchType, "ByLanguage", new { id = "searchType_ByLanguage" })


			        <br />
                    <br />
                    <strong>Tag</strong>
                    <div>
                        @Html.TextBoxFor((x) => x.SearchForTag, new { @class = "textbox" })
                         
                        @Html.ValidationMessageFor((x) => x.SearchForTag)
                    </div>
                
                    <strong>Key Word</strong>
                    <div>
                        @Html.TextBoxFor((x) => x.SearchForKeyWord, new { @class = "textbox" })
                         
                        @Html.ValidationMessageFor((x) => x.SearchForKeyWord)
                    </div>

                    <strong>Language</strong>
                    <div>
                        @Html.ComboFor(x => x.LanguageId, 
			    x => x.LanguageList, 
			    x => x.LanguageId, 
			    x => x.Language1, 
                            new Dictionary<string,object> { 
                                { "style", "width:400px"}, 
                                { "class", "selectBox"  }})
                         
                        @Html.ValidationMessageFor((x) => x.LanguageId)
                    </div>

                </div>
	        </div>

 
            <div class="headedPanel">
        	    <div class="stepPanel">
			        <br />
                    <strong>Visibility<strong>
                    <div>
                     @Html.ComboFor(x => x.Visibility, 
			x => x.VisibilityList, 
			x => x.Id, 
			x => x.VisibilityDescription,
                        new Dictionary<string, object> { 
                            { "style", "width:400px"}, 
                            { "class", "selectBox"  }})
                     
                    @Html.ValidationMessageFor((x) => x.Visibility)
                    </div>
                </div>
            </div>
        
        
            <div style="margin-top:20px;margin-left:25px">
                <span class="btn"><a id="SearchSubmit">Submit</a><span></span></span>
                <span class="clear"></span>
                <br />
                <br /> 
            </div>
        
        </div>
 
        <div id="loader" style="display:none">
            @Html.Partial("_Loader")
        </div>
     
    }
</div>

That form is pretty simple it basically just provides the form elements to allow the user to enter their required search, which is posted to the SearchController's CreateSearch action, which we will see in a minute

Let's now turn our attention to the JavaScript for this page, which doesn't do too much apart from submit the form to the SearchController's Search action

$(document).ready(function () {

	InitBinding();
});



function InitBinding() {

    //	On submit on add page, submit the add and shows the success
    //	page it the edit was successful, otherwise add page is shown again including
    //	validation errors
    $("#SearchSubmit").click(function (e) {
        e.preventDefault();

        $("#CreateSearchForm").submit();
        $('#allFormData').hide();
        $('#loader').show();
    });


    if ($('successfulCreate').length > 0) {
        var successfulCreate = $('#successfulCreate').val();
        if (successfulCreate == "False") {
            showOkDialog('Your search is invalid', 180, 'Error');
        }
    }

}

Ok the JavaScript does do a bit more to deal with the Telerik ASP MVC Extensions DataGrid, but we will see the extra bits in a minute

So let's now see what happens inside the SearchController's CreateSearch action. There are several things to not here which are as follows:

  • Since the search could take a while I took the decision to do this asynchronously using Task Parallel Library
  • Since I am doing some work asynchronously this controller inherits from AsyncController

Suprisingly enough the SearchController is not that bad here are the most relevant parts

public class SearchController : BaseTagCloudEnabledAsyncController
{
    public SearchController(
        ILoggerService loggerService, 
        IMembershipService membershipService,
        ITagCloudService tagCloudService,
        IRepository<Language> languageRepository, 
        IRepository<Visibility> visibilityRepository,
        IRepository<CodeCategory> categoryRepository,
        IRepository<Grouping> groupingRepository,
        IRepository<CodeSnippet> codeSnippetRepository,
        IRepository<CodeTag> codeTagRepository,
        IRepository<CreatedTeam> createdTeamRepository,
        IUnitOfWork unitOfWork)
        : base(tagCloudService)
    {
        .....
    }



    [Authorize]
    [RenderTagCloud]
    [HttpPost]
    [ValidateAntiForgeryToken(Salt = "CreateSearch")]
    public void CreateSearchAsync(CreateSearchViewModel vm)
    {
        AsyncManager.OutstandingOperations.Increment();

        if (ModelState.IsValid)
        {
            ViewData["successfulCreate"] = true;
            try
            {
                MembershipUser user = membershipService.GetUserByUserName(User.Identity.Name);
                Guid userId = Guid.Parse(user.ProviderUserKey.ToString());
                SearchTaskState searchTaskState = new SearchTaskState(ViewData, vm, userId);

                Task<ShowSearchResultsViewModel> searchTask = CreateSearchTask(searchTaskState);
                searchTask.Start();
                searchTask.ContinueWith((ant) =>
                {
                    SetValidSearchAsyncResult(AsyncManager, ant.Result);
                    AsyncManager.OutstandingOperations.Decrement();
                }, TaskContinuationOptions.OnlyOnRanToCompletion);

                searchTask.ContinueWith((ant) =>
                {
                    SetInvalidAsyncResult(AsyncManager, vm);
                    AsyncManager.OutstandingOperations.Decrement();
                }, TaskContinuationOptions.OnlyOnFaulted);

            }
            catch (AggregateException ex)
            {
                SetInvalidAsyncResult(AsyncManager, vm);
                AsyncManager.OutstandingOperations.Decrement();
            }
        }
        else
        {
            SetInvalidAsyncResult(AsyncManager, vm);
            AsyncManager.OutstandingOperations.Decrement();
        }

    }

    public ActionResult CreateSearchCompleted(bool wasValid, object vm)
    {
        if (!wasValid)
        {
            return View("CreateSearch", vm);
        }
        else
        {
            return View("ShowSearchResults", vm);
        }
    }


    private void SetInvalidAsyncResult(AsyncManager asyncManager, CreateSearchViewModel vm)
    {
        ViewData["successfulCreate"] = false;
        using (unitOfWork)
        {
            RefreshModelStaticData(vm);
        }
        asyncManager.Parameters["wasValid"] = false;
        asyncManager.Parameters["vm"] = vm;
    }


    private void SetValidSearchAsyncResult(AsyncManager asyncManager, ShowSearchResultsViewModel vm)
    {
        AsyncManager.Parameters["wasValid"] = true;
        AsyncManager.Parameters["vm"] = vm;
    }




    private Task<ShowSearchResultsViewModel> CreateSearchTask(SearchTaskState searchTaskState)
    {
        return new Task<ShowSearchResultsViewModel>((state)=>
            {
                SearchTaskState taskState = (SearchTaskState)state;
                ShowSearchResultsViewModel searchResultsVm;

                using (unitOfWork)
                {
                    .....
                    .....
                    .....
                    .....
                    Tuple<int, List<CodeSnippet>> results =
                        SearchUtils.FilterByVisibility(
                            unitOfWork,
                            createdTeamRepository,
                            codeTagRepository,
                            languageRepository,
                            codeSnippetRepository,
                            taskState.Vm.SearchType,
                            searchValue.ToLower(),
                            1,
                            1,
                            false,
                            visibility,
                            taskState.UserId,
                            tags);

                    Session["searchResults"] = results.Item2;

                    searchResultsVm =
                        new ShowSearchResultsViewModel(
                            taskState.Vm.SearchType,
                            languageRepository.FindBy(x => x.LanguageId == taskState.Vm.LanguageId).Single(),
                            visibilityRepository.FindBy(x => x.Id == taskState.Vm.Visibility).Single(),
                            searchValue,
                            results.Item1,
                            results.Item2);
                }
                return searchResultsVm;
            }, searchTaskState);


    }



    private string GetSearchValueBasedOnSearchType(CreateSearchViewModel vm)
    {
        string searchValue = "";
        switch (vm.SearchType)
        {
            case SearchType.ByKeyWord:
                searchValue = vm.SearchForKeyWord;
                break;
            case SearchType.ByTag:
                searchValue = vm.SearchForTag;
                break;
            case SearchType.ByLanguage:
                languageRepository.EnrolInUnitOfWork(unitOfWork);
                searchValue = languageRepository.FindBy(x => x.LanguageId == vm.LanguageId).Single().Language1;
                break;
        }
        return searchValue;
    }
}

It can be seen that this controller actually returns a new view once the search resulst are obtained which is called ShowSearchResultsView which can be seen below, this is the one we saw a screen shot of earlier with the Telerik ASP MVC Extensions DataGrid.  

@model CodeStash.Models.Search.ShowSearchResultsViewModel
@using Telerik.Web.Mvc.UI
@using CodeStash.ExtensionsMethods
@{
    ViewBag.Title = "Create Search";
    Layout = "~/Views/Shared/_Layout.cshtml";
}

@section SpecificPageHeadStuff
 {  
     @Html.CssTag(Url.Content("~/Content/Controllers/Search/search.css"))
     @Html.ScriptTag(Url.Content("~/Scripts/Controllers/Search/search.js"))
}

<div id="dialog-message" style="display: none;">
    <p id="dialog-message-content">
    </p>
</div>
<div id="SearchPanel">

    <h2>Showing Search Results</h2>
    <p>Your search was</p>
    <ul>
        <li>Visibility : '@Model.Visibility.VisibilityDescription'</li>
        @switch (Model.SearchType)
        {
            case CodeStash.Common.Enums.SearchType.ByKeyWord:
                   <li>Search Type : 'ByKeyWord', where your search term was :  '@Model.SearchValue'</li>
                    break;
            case CodeStash.Common.Enums.SearchType.ByLanguage:
                   <li>Search Type : 'ByLanguage', where your search language was : '@Model.Language.LanguageCode'</li>
                    break;
            case CodeStash.Common.Enums.SearchType.ByTag:
                   <li>Search Type : 'ByTag', where your search term was : '@Model.SearchValue'</li>
                   break;
        }
    </ul>


    @(Html.Telerik().Grid<CodeStash.Common.DataAccess.EntityFramework.CodeSnippet>()
        .ScriptFilesPath("~/Scripts/Telerik")
        .HtmlAttributes(new { style = "margin:0px" })
        .Name("SearchResultsGrid")
            .DataBinding(dataBinding => dataBinding
                //Ajax binding
                .Ajax()
                //The action method which will return JSON
                .Select("_AjaxBinding", "Search")
            )
            .Columns(columns =>
            {

                columns.Bound(o => o.CodeSnippetId).Title("Id").HeaderHtmlAttributes(new { @class = "id-column" }).ReadOnly(true);
                columns.Bound(o => o.Description).ReadOnly(true);
                columns.Bound(o => o.Title).ReadOnly(true);
                columns.Bound(o => o.CodeSnippetId).ClientTemplate("<a href=\"#\" class=\"popup-button\">View Details</a>")
                    .Title("Popup").Filterable(false).Sortable(false).Width(130).ReadOnly(true);
                columns.Bound(o => o.CodeSnippetId).ClientTemplate("<a href=\"#\" class=\"show-button\">View Details</a>")
                    .Title("Show").Filterable(false).Sortable(false).Width(130).ReadOnly(true);
            })
            .ClientEvents(e => e.OnRowDataBound("SearchResultsGrid_onRowDataBound"))
            .Pageable()
            .Scrollable(scroll => scroll.Height(200))
            .Filterable()
            .Sortable(sorting => sorting
            .SortMode(GridSortMode.SingleColumn)
                .OrderBy(o => o.Add(p => p.CodeSnippetId).Ascending()))

    )
</div>

It can be seen that this makes use of the Telerik ASP MVC Extensions (which are completely free, thanks Telerik).

This grid supports various different models, but the model that I have gone for is the Ajax updating model, which allows the DataGrid to update using a server side method which is called when paging occurs. This server side controller method is as shown below.

[GridAction]
public ActionResult _AjaxBinding()
{
    return View(new GridModel((IEnumerable)Session["searchResults"]));
}

In order for this free Telerik DataGrid  to work properly we need to do a few things in the master page "_Layout.cshtml", which is shown below

@( Html.Telerik().StyleSheetRegistrar().DefaultGroup(group => group
    .DefaultPath("~/Content/Telerik")
    .Add("telerik.common.css")
    .Add("telerik.Black.min.css"))
)

The last peice to search is the rest of the JavaScript which I purposely did not show you early, this is now shown below. It can be seen that the JavaScript takes care of binding the 2 hyperlink grid columns and also has jQuery Ajax calls to either redirect or fetch a single snippets code when one of the DataGrid hyper link columns is clicked.

//Telerik grid setup
function SearchResultsGrid_onRowDataBound(e) {
    var dataItem = e.dataItem;
    var snippetId = dataItem.CodeSnippetId;

    $(e.row).find("a.popup-button")
                    .click(function (e) {
                        ShowSnippetPopup(snippetId);
                    });

    $(e.row).find("a.show-button")
                .click(function (e) {
                    window.location.href = '/CodeSnippet/DisplaySnippetsForAddAndEdit' + 
			'?codeSnippetId=' + snippetId + "&wasAddOrEdit=false";
                });
}


function ShowSnippetPopup(snippetId) {


    $.post("/Search/GetSpecificSnippetData", { snippetId: snippetId },
            function (data) {
                if (data.Success != undefined && data.Success) {

                    var codeSnippetContents = '<pre id="preCode_' + snippetId + '" class="' + 
			data.PreClass + ' searchsnippetPopup">' + data.CodeSnippetCode + '</pre>'
                    	showDialogWidthAndHeightNoCallbackAndNoScroll(codeSnippetContents,
                        600, 450, 'Displaying Snippet : ' + snippetId);
                    SwitchHightlighting(data.HighlightingCSSName);
                }
                else {
                    showOkDialog(data.Message, 180, 'Error');
                }
            },
            "json");

}

And that is pretty much how search works

Display Snippets

The displaying of snippets is handled by the CodeSnippetController, where there are 2 main methods that handle the displaying of snippets.

  • DisplaySnippetsForAddAndEdit : This action is called after a successful Add or Edit has been peformed, and will display the Added snippet or Edited snippet. If that snippet is part of a group all snippets in that group will also be shown
  • DisplaySnippetsForCategory : This action is called when a user clicks on one of the entries in the Tag Cloud.
namespace CodeStash.Controllers
{
	public class CodeSnippetController : BaseTagCloudEnabledController
	{

	        [Authorize]
        	[RenderTagCloud]
        	[HttpGet]
        	public ActionResult DisplaySnippetsForAddAndEdit(int codeSnippetId, bool wasAddOrEdit=true)
        	{
		....
		....
		....
		....

		}

 



	        [Authorize]
       	 	[RenderTagCloud]
        	[HttpGet]
        	public ActionResult DisplaySnippetsForCategory(string category)
        	{
		....
		....
		....
		....

		}
}

The DisplaySnippets full page view is shown in either case, where it is passed the following ViewModel

namespace CodeStash.Models.Snippet
{
    public class DisplaySnippetsViewModel
    {
        public static int MAX_SNIPPETS_TO_DISPLAY = 100;

        public DisplaySnippetsViewModel(
            List<CodeSnippetWrapperViewModel> codeSnippets, 
            DisplayMode displayMode,
            bool isTruncated,
            String highlightingCSS)
        {
            this.CodeSnippets = codeSnippets;
            this.DisplayMode = displayMode;
            this.IsTruncated = isTruncated;
            this.HighlightingCSS = highlightingCSS;
            IsGrouped = this.CodeSnippets.Any(x => x.CodeSnippet.GroupId.HasValue);

        }


        public bool IsGrouped { get; private set; }
        public List<CodeSnippetWrapperViewModel> CodeSnippets { get; private set; }
        public DisplayMode DisplayMode { get; private set; }
        public bool IsTruncated { get; private set; }
        public string HighlightingCSS  { get; private set; }

    }



    public class CodeSnippetWrapperViewModel
    {
        public bool IsSnippetEditable { get; private set; }
        public CodeSnippet CodeSnippet { get; private set; }
 
        public CodeSnippetWrapperViewModel(bool isSnippetEditable, CodeSnippet codeSnippet)
        {
            this.IsSnippetEditable = isSnippetEditable;
            this.CodeSnippet = codeSnippet;


        }
    }
}

Here is the markup for the full page view, see how it shows another partial view DisplaySnippetsPartial

@model CodeStash.Models.Snippet.DisplaySnippetsViewModel
@{
	ViewBag.Title = "Grouped Snippets";
	Layout = "~/Views/Shared/_Layout.cshtml";
}
@using CodeStash.ExtensionsMethods

@section SpecificPageHeadStuff
 {
     @Html.CssTag(Url.Content("~/Content/Controllers/DisplaySnippets/displaySnippets.css"))
     @Html.ScriptTag(Url.Content("~/Scripts/Controllers/DisplaySnippets/displaySnippets.js"))

}



<div id="dialog-message"  style="display:none;">
	<p id="dialog-message-content">
	</p>
</div>

<div id="DisplaySnippetPanel">
    
    <h2>Code Snippets</h2>
    <input id="WasAddOrEdit" type="hidden" value="@ViewData["WasAddOrEdit"]" />
    <input id="DisplayMode" type="hidden" value="@Model.DisplayMode" />
    <input id="HighlightingCSSToUse" type="hidden" value="@Model.HighlightingCSS" />
    @Html.Partial("DisplaySnippetsPartial",Model)
    


</div>

And here is the most relevant parts of the partial view DisplaySnippetsPartial

@model CodeStash.Models.Snippet.DisplaySnippetsViewModel
           
<div id="AjaxContents">
<p id="noSnippetsMessage" style="display:none;">There are no CodeSnippets to display</p>

@if (Model.DisplayMode == CodeStash.DisplayMode.SingleSnippet)
{
    if (Model.IsGrouped)
    {
        <p id="snippetsMessage">As the requested snippet is part of a group, displaying all Code Snippets in Group 
        [@Model.CodeSnippets.First().CodeSnippet.Grouping.Description]</p>
    }
    else
    {
        <p id="snippetsMessage">The requested snippet is shown below</p>
    }
}

@foreach (CodeStash.Models.Snippet.CodeSnippetWrapperViewModel snippet in Model.CodeSnippets)
{
    @Html.Partial("SingleSnippetPartial", snippet);
}
    
    
</div>

It can be seen that this partial view also makes use of yet another partial view called SingleSnippetPartial, which as its name suggests is responsible for rendering a single snippet. The SingleSnippetPartial and the accompanying JavaScript provide the following functions

  • Collapse of the current snippet
  • Expand of the current snippet
  • Edit of the current snippet
  • Delete of the current snippet
  • Provide shareable link to the current snippet (which when shared allows non CodeStash users to view a Read Only version of the snippet)

Anyway here is what the SingleSnippetPartial  markup looks like

@model CodeStash.Models.Snippet.CodeSnippetWrapperViewModel

<div id="codeSnippet-@Model.CodeSnippet.CodeSnippetId" class="snippet">
    <p>
        <strong>ID:&nbsp</strong>@Model.CodeSnippet.CodeSnippetId<br />
        <strong>Title:&nbsp</strong>@Model.CodeSnippet.Title<br />
        <strong>Description:&nbsp</strong>@Model.CodeSnippet.Description<br />
        <strong>Category:&nbsp</strong>@Model.CodeSnippet.CodeCategory.CodeCategoryName<br />
    </p>


    <div class="highlightedSnippetHeader">
    

            <div class="snippetActionArea link">
                <img src="../../Content/images/link.png" width="25px" alt="" />
            </div>
            <div class="tooltip">Get link for this code snippet</div>


            <div class="snippetActionArea collapse">
                <img src="../../Content/images/collapse.png" width="25px" alt="" />
            </div>
            <div class="tooltip">Collapse code snippet</div>

            <div class="snippetActionArea expand">
                <img src="../../Content/images/expand.png" width="25px"  alt=""/>
            </div>
            <div class="tooltip">Expand code snippet</div>

            @if (Model.IsSnippetEditable)
            {
                <div class="snippetActionArea delete enabled">
                    <img src="../../Content/images/delete.png" width="25px" alt=""/>
                </div>
                <div class="tooltip">Delete code snippet</div>
        
                <div class="snippetActionArea edit enabled">
                    <img src="../../Content/images/edit.png" width="25px"  alt=""/>
                </div>
                <div class="tooltip">Edit code snippet</div>
            }
            else
            {
                <div class="snippetActionArea delete disabled">
                    <img src="../../Content/images/deleteDisabled.png" width="25px" alt=""/>
                </div>
                <div class="tooltip">Delete code snippet</div>
        
                <div class="snippetActionArea edit disabled">
                    <img src="../../Content/images/editDisabled.png" width="25px"  alt=""/>
                </div>
                <div class="tooltip">Edit code snippet</div>
            }
    </div>
    <div class="highlightedSnippet">
    @if (Model.CodeSnippet.Grouping != null)
    {
        <input class="isGrouped" type="hidden" value="true" />
        <input class="groupId" type="hidden" value="@Model.CodeSnippet.GroupId" />
        <input class="groupDescription" type="hidden" value="@Model.CodeSnippet.Grouping.Description" />
        
    }
    else
    {
        <input class="isGrouped" type="hidden" value="false" />
    }
    <input class="codeSnippetId" type="hidden" value="@Model.CodeSnippet.CodeSnippetId" />
    <pre class="@CodeStash.Utils.WebSiteUtils.GetCodeClass(Model.CodeSnippet.Language.LanguageCode)">
        @Model.CodeSnippet.ActualCode.Trim()</pre>
</div>
    <br />
</div>

Anyway here is the end result of displaying a snippet

It can be seen that there are number of images which act as button which allow the various functions, such as Collapse/Expand/Edit/Delete/Share link to be performed, we will get into this in a minute when look aat the JavaScript side of thing.

But how about the snippet highlighting, how does that work. Well that is actually pretty simple, we just need to ensure that the code rendered is wrapped in a PRE that has a class which is partially made up of the stored snippets language, since we store that in the database that information is freely available.

So this would end up with a PRE something like this

<pre class="csharpCode">

That is all that needs to be done on a individual snippet level, this of course not the full story, there is a 3rd party free JavaScript library that we make use of that actually does the highlighting.

I searched long and hard for the best way to do syntax highlighting in a CodeStash prototype I initially constructed, and I was fortunate enough to find a very good javascript synytax highlighting library, which is available from :

http://www.steamdev.com/snippet/

This library comes with all the required files, and shown below are the steps needed to use it. Though it does come with a very good work through on the web site too, see the web sites “Usage” section.


CSS

Include a link to the jquery.snippet.css file. The best place for this is the MasterPage "_Layout.cshtml"

@Html.CssTag(Url.Content("~/Content/Highlighting/jquery.snippet.css"))

jQuery Snippet Plugin

Include a link the jquery.snippet.js file. The best place for this is the MasterPage "_Layout.cshtml"

@Html.ScriptTag(Url.Content("~/Scripts/Highlighting/jquery.snippet.js"))

And finally here is the most relevant parts of the JavaScript for snippet highlighting.

It can be seen that is has hooks for all the previously mentioned functions Collapse/Expand/Edit/Delete/Share snippet link

$(document).ready(function () {

    InitBinding();
});


function InitBinding() {

    SwitchHightlighting($("#HighlightingCSSToUse").val());

    $(".link img").click(function () {

        var snippetElement = $(this).closest(".highlightedSnippetHeader").next(".highlightedSnippet");
        var codeSnippetId = snippetElement.find(".codeSnippetId").val();

        showNoButtonsDialog(
                '<p><strong>Snippet link</strong><br/><i>' + window.location.origin +
                '/Readonly/Display/' + codeSnippetId + '</i><br/><br/>' +
                'Copy the link to share it' +
                '</p>', 195, 'Information'
            );
    });


    $(".delete.enabled img").click(function () {

        var displayMode = $('#DisplayMode').val();
        var snippetElement = $(this).closest(".highlightedSnippetHeader").next(".highlightedSnippet");
        var isGrouped = snippetElement.find(".isGrouped").val();
        var groupDescription = snippetElement.find(".groupDescription").val();
        var groupId = snippetElement.find(".groupId").val();
        var codeSnippetId = snippetElement.find(".codeSnippetId").val();

        if (isGrouped == "true") {

            showYesNoGroupSnippetDialog("Code snippet " + codeSnippetId + " is part of group '" + groupDescription +
                "'.<br/><br/> Please pick whether to delete just the single selected snippet or ALL snippets in the group",
                220, "Confirm Delete",
                function (data) {
                    DeleteSnippetAndShowRemaining(displayMode, codeSnippetId, groupId, false);
                },
                function (data) {
                    DeleteSnippetAndShowRemaining(displayMode, codeSnippetId, groupId, true);
                }
            );
        }
        else {
            showYesNoDialog('Are you sure you want to delete this snippet?', 180, 'Confirm',
                function () {
                    DeleteSnippetAndShowRemaining(displayMode, codeSnippetId, groupId, false);
                }
            );
        }
    });


    $(".edit.enabled img").click(function () {

        var snippetElement = $(this).closest(".highlightedSnippetHeader").next(".highlightedSnippet");
        var codeSnippetId = snippetElement.find(".codeSnippetId").val();
        window.location.href = '/CodeSnippet/Edit' + '?codeSnippetId=' + codeSnippetId;

    });

}

function DeleteSnippetAndShowRemaining(displayMode, snippetId, groupId, deleteAllInGroup) {

    $.post("/CodeSnippet/DeleteSnippetAndShowRemaining",
            {
                'displayMode': displayMode,
                'snippetId': snippetId,
                'groupId': groupId,
                'deleteAllInGroup': deleteAllInGroup
            },
            function (response) {
            ....
            ....
            ....
            ....
            ....
            },
            'json'
    );
}

There is a single line near the top of that jQuery that deals with highlighting the snippets using the users chosen highlighting snippet theme (which is stored against their Profile in the ASP .NET Membership database. That line in this one

SwitchHightlighting($("#HighlightingCSSToUse").val());

Where the SwitchHightlighting method looks like this

function SwitchHightlighting(stylename) {

    $("pre.c" + "Code").snippet("c", { style: stylename, showNum: false });
    $("pre.cpp" + "Code").snippet("cpp", { style: stylename, showNum: false });
    $("pre.csharp" + "Code").snippet("csharp", { style: stylename, showNum: false });
    $("pre.css" + "Code").snippet("css", { style: stylename, showNum: false });
    $("pre.flex" + "Code").snippet("flex", { style: stylename, showNum: false });
    $("pre.html" + "Code").snippet("html", { style: stylename, showNum: false });
    $("pre.java" + "Code").snippet("java", { style: stylename, showNum: false });
    $("pre.javascript" + "Code").snippet("javascript", { style: stylename, showNum: false });
    $("pre.javascript_dom" + "Code").snippet("javascript_dom", { style: stylename, showNum: false });
    $("pre.perl" + "Code").snippet("perl", { style: stylename, showNum: false });
    $("pre.php" + "Code").snippet("php", { style: stylename, showNum: false });
    $("pre.python" + "Code").snippet("python", { style: stylename, showNum: false });
    $("pre.ruby" + "Code").snippet("ruby", { style: stylename, showNum: false });
    $("pre.sql" + "Code").snippet("sql", { style: stylename, showNum: false });
    $("pre.xml" + "Code").snippet("xml", { style: stylename, showNum: false });
}

The snippet highlighting library comes with 33 themes to choose from.

Edit Snippet

This page allow logged in users to edit an existing code snippet (providing its yours). It starts by finding a snippet you want to edit

Then after you click the edit icon, you will be redirected to the edit snippet page which is as shown below

The edit page works much the same as the Add page, apart from the fact that a few of the fields are now readonly. And since we just discussed how the Add Snippet page works, I will leave it up to you imagination how the edit one works, you are a clever lot I am sure you imagine it.

Delete Snippet

The whole idea behind CodeStash is that its a snippet portal, and that you can manage your snippet respository. As such you can obviously choose to delete a snippet (providing its yours), which starts by finding a snippet you want to delete. Once you find a snippet you may use the delete icon which will show you a popup asking you to confirm your delete. Now remember that code snippets can be grouped (you know C#/ASPX/HTML all being in a logical group), as such the delete dialog may ask you to delete ALL snippets in group, if the current snippet is in a group. Or if there is no groupong you will see a standard confirm dialog

This is what is shown if the snippet is in a group

This is what is shown if the snippet is NOT in a group

So how does all this work behind the scenes? Its actually suprsingly simple really, if we go back to look at the schema for a minute

It is pretty clear that each CodeSnippet is capable of being in a group, though it is not mandatory. So based on that it's really just a question of getting the fact that the snippet is in a group into the view (I used a hidden field to do this), and then let the jQuery check whether there is a group or not, which shows the correct dialog.

@if (Model.CodeSnippet.Grouping != null)
{
    <input class="isGrouped" type="hidden" 
	value="true" />
    <input class="groupId" type="hidden" 
	value="@Model.CodeSnippet.GroupId" />
    <input class="groupDescription" type="hidden" 
	value="@Model.CodeSnippet.Grouping.Description" />
        
}
else
{
    <input class="isGrouped" type="hidden" value="false" />
}

And here is the relevant jQuery

$(".delete.enabled img").click(function () {

    var displayMode = $('#DisplayMode').val();
    var snippetElement = $(this).closest(".highlightedSnippetHeader").next(".highlightedSnippet");
    var isGrouped = snippetElement.find(".isGrouped").val();
    var groupDescription = snippetElement.find(".groupDescription").val();
    var groupId = snippetElement.find(".groupId").val();
    var codeSnippetId = snippetElement.find(".codeSnippetId").val();

       

    if (isGrouped == "true") {

        showYesNoGroupSnippetDialog("Code snippet " + codeSnippetId + 
		" is part of group '" + groupDescription +
            "'.<br/><br/> Please pick whether to delete just the single selected snippet or ALL snippets in the group",
            220, "Confirm Delete",
            function (data) {
                DeleteSnippetAndShowRemaining(displayMode, codeSnippetId, groupId, false);
            },
            function (data) {
                DeleteSnippetAndShowRemaining(displayMode, codeSnippetId, groupId, true);
            }
        );
    }
    else {
        showYesNoDialog('Are you sure you want to delete this snippet?', 180, 'Confirm',
            function () {
                DeleteSnippetAndShowRemaining(displayMode, codeSnippetId, groupId, false);
            }
        );
    }
});

Where the following controller code is called to do the actual delete

[Authorize]
[HttpPost]
[AjaxOnly]
public JsonResult DeleteSnippetAndShowRemaining(DisplayMode displayMode, 
    int snippetId, int groupId, bool deleteAllInGroup)
{
    ....
    ....
}

 

ReadOnly Display Snippets

This works in much the same way as the NON read only snippets, with the exception of it using a minimal master page that doesn't have any of the extra fluff, its a simple view that just shows snippets, and you can no longer edit/delete the snippet as these functions require you to be logged into to CodeStash

Addin Rest API

The CodeStash Addin that Pete O'Hanlon has written obviously needs to get its data from somewhere and also needs a place to store its data. That is obviously the centralised (for the moment, it may change to cloud hosting/no sql depending on uptake) SQL server database.

There were a couple of different options available here such as those shown below, but we wanted an open architecture that was open to any sort of client, not just windows ones.

  1. WCF : Felt to strict, and was Windows only.
  2. Web WCF Api : Ok slightly better, but still meant server has to be written differently from web site
  3. Standard controller that supports JSON CRUD. Nice this hits the sweet spot, as its completely standard JSON, and it all POST/GET based without any magic. This is what we went for

So now that we know that there is a dedicated controller for the REST API that the CodeStash Addin uses what does it look like. Well its prety simply if you just consider its methods

Lets see those.

namespace CodeStash.Controllers
{
    public class RestController : Controller
    {

        #region Ctor

        public RestController(IGetUserForRestService getUserForRestService,
                                ILoggerService loggerService,
                                IRepository<CodeSnippet> codeSnippetRepository,
                                IRepository<CodeCategory> codeCategoryRepository,
                                IRepository<CodeTag> codeTagRepository,
                                IRepository<Grouping> groupingRepository,
                                IRepository<Language> languageRepository,
                                IRepository<CreatedTeam> createdTeamRepository,
                                IRepository<Visibility> visibilityRepository,
                                IUnitOfWork unitOfWork)
        {
	    ....
	    ....

        }
        #endregion

        #region REST API
        /// <summary>
        /// Searches all available <c>CodeSnippet</c>(s) and returns a 
	    /// List<c>JSONPagesSearchResultCodeSnippet</c>(s)
        /// </summary>
        [JSONInput(Param = "input", RootType = typeof(JSONSearchInput))]
        public ActionResult Search(JSONSearchInput input)
        {
	    ....
	    ....

        }



        /// <summary>
        /// Adds a new <c>CodeSnippet</c> and returns a 
	    /// <c>JSONCodeSnippetAddSingleResult</c>
        /// with the newly added <c>CodeSnippet</c> or an Exception that may have occurred
        /// </summary>
        [JSONInput(Param = "input", RootType = typeof(JSONAddSnippetInput))]
        public ActionResult AddSnippet(JSONAddSnippetInput input)
        {
	    ....
	    ....

        }


        /// <summary>
        /// Gets all <c>Language</c>(s) which are returned in a 
	    ///  <c>JSONLanguages</c>
        /// with all the <c>Language</c>(s) or an Exception that may have occurred
        /// </summary>
        [JSONInput(Param = "input", RootType = typeof(JSONCredentialInput))]
        public ActionResult GetAllLanguages(JSONCredentialInput input)
        {
	    ....
	    ....
        }


        /// <summary>
        /// Gets all <c>Grouping</c>(s) which are returned in a 
	    /// <c>JSONGrouping</c>
        /// with all the <c>Grouping</c>(s) or an Exception that may have occurred
        /// </summary>
        [JSONInput(Param = "input", RootType = typeof(JSONCredentialInput))]
        public ActionResult GetAllGroups(JSONCredentialInput input)
        {
	    ....
	    ....
		
        }
        #endregion


    }
}

This is really all there is to it, obviously there are CRUD operation going on in these methods, but that is largely irrelevant. There are 2 important things to note here. One being that ALL rest API calls MUST provide a JSONCredentialInput, this means that calls that supply a correct well formed validated JSONCredentialInput will be able to use the rest API. The second one being that it is a standard MVC approach of exposing JSON data, and accepting new JSON data using model binding.

Accepting model bound JSON data is achieved using the specialised ActionFilter JSONInputAttribute which looks like this

public class JSONInputAttribute : ActionFilterAttribute
{
    public string Param { get; set; }

    public Type RootType { get; set; }

    private Encoding GetEncoding(string contentType)
    {
        Encoding encoding = Encoding.Default;

        switch (contentType)
        {
            case "application/json; charset=UTF-7":
                encoding= Encoding.UTF7;
                break;
            case "application/json; charset=UTF-8":
                encoding= Encoding.UTF8;
                break;
            case "application/json; charset=unicode":
                encoding= Encoding.Unicode;
                break;
            case "application/json; charset=ascii":
                encoding= Encoding.ASCII;
                break;
        }

        return encoding;
    }


    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        try
        {
            string json = filterContext.HttpContext.Request.Form[Param];

            if (json == "[]" || json == "\",\"" || String.IsNullOrEmpty(json))
            {
                filterContext.ActionParameters[Param] = null;
            }
            else
            {
                Encoding encoding = GetEncoding(filterContext.HttpContext.Request.ContentType);

                using (var ms = new MemoryStream(encoding.GetBytes(json)))
                {
                    filterContext.ActionParameters[Param] =
			new DataContractJsonSerializer(RootType).ReadObject(ms);
                }
            }
        }
        catch
        {
            filterContext.ActionParameters[Param] = null;
        }
    }

}

It can be seen that this makes use of the DataContractJsonSerializer that is due to the fact that the .NET client makes of the same serialization process. This however doesn't matter and this ActionFilter is totally capable of accepting JSON from anywhere, it just uses the DataContractJsonSerializer to hydrate the JSON data back into .NET objects for the controller. Lets see one of these JSON DTO objects that the Addin uses

[DataContract]
public partial class JSONLanguage
{

    public JSONLanguage(int languageId, string language)
    {
        this.LanguageId = languageId;
        this.Language = language;
    }

    [DataMember]
    public int LanguageId { get; set; }

    [DataMember]
    public string Language { get; set; }
}

And here is sneak peak example of how the addin may call into the REST API. Rest (pardon the pun) Pete will be covering this in more detail in his article(s) on the Addin.

/// <summary>
/// Retrieve the languages from the service.
/// </summary>
public JSONLanguagesResult RetrieveLanguages()
{
    return GetValue<JSONLanguagesResult>("GetAllLanguages");
}


private T GetValue<T>(string restService) where T : class
{
    return Utilities.GetValue<T>(GetDataFromRestService(restService));
}



protected byte[] GetDataFromRestService(string restMethod)
{
    JSONCredentialInput input = new JSONCredentialInput(
        openId, // If not specified, will be an empty string but the password must be set.
        emailAddress, // Must always be present.
        password // If not specified, will be an empty string, but the OpenID must be set.
        );
    return CallService(input, CodeStash.Common.Helpers.ConfigurationSettings.RestAddress, restMethod);
}


private byte[] CallService<T>(T input, string address, string methodToCall)
{
    values = new NameValueCollection();
    Utilities.AddValue(values, "input", input);

    WebClient client = new WebClient();
    return client.UploadValues(string.Format("{0}{1}", address, methodToCall), values);
}

Where the following utility code is responsible for serializing/derserialization as JSON

using System;
using System.Linq;
using System.Text;
using CodeStash.Common.Encryption;
using System.Runtime.Serialization.Json;
using System.IO;
using System.Collections.Specialized;
using System.Collections.Generic;

namespace CodeStash.Addin.Core
{
    public static class Utilities
    {
        internal static DataContractJsonSerializer jss;

        public static string GetStringForWebsiteCall(this string value)
        {
            if (string.IsNullOrWhiteSpace(value))
                return string.Empty;

            if (EncryptionHelper.EncryptionEnabled)
                return EncryptionHelper.GetEncryptedValue(value);
            return value;
        }

        internal static T GetValue<T>(Byte[] results) where T : class
        {
            using (MemoryStream ms = new MemoryStream(results))
            {
                jss = new DataContractJsonSerializer(typeof(T));
                return (T)jss.ReadObject(ms);
            }
        }


        internal static void AddValue(NameValueCollection values, string key, object value)
        {
            jss = new DataContractJsonSerializer(value.GetType());
            using (MemoryStream ms = new MemoryStream())
            {
                jss.WriteObject(ms, value);
                string json = Encoding.UTF8.GetString(ms.ToArray());
                values.Add(key, json);
            }
        }
    }
}

That's It

Anyway there you have it, I hope you all like the work Pete and I have put together, we have both spent lots of time on this and we both believe it could be a most useful tool. As we say we would love to hear from you on this one, do you think its useful would you use it? What imrovements could we make? It really would be good to hear from you all on your thoughts, we have tried to make it work the way we think would be best for developers, but we have probably missed somethings, so we are kind of reliant on you guys to tell us that, so please don't hold back if there are things you want us to do for V2. We have have a few things up our sleeves for V2 but we wanted to see what the general feeling was for this 1st offering before we set about sweating more blood and tears over this project.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Authors

Pete O'Hanlon
CEO
United Kingdom United Kingdom
A developer for over 30 years, I've been lucky enough to write articles and applications for Code Project as well as the Intel Ultimate Coder - Going Perceptual challenge. I live in the North East of England with 2 wonderful daughters and a wonderful wife.
 
I am not the Stig, but I do wish I had Lotus Tuned Suspension.
Follow on   Twitter   Google+

Sacha Barber
Software Developer (Senior)
United Kingdom United Kingdom
I currently hold the following qualifications (amongst others, I also studied Music Technology and Electronics, for my sins)
 
- MSc (Passed with distinctions), in Information Technology for E-Commerce
- BSc Hons (1st class) in Computer Science & Artificial Intelligence
 
Both of these at Sussex University UK.
 
Award(s)

I am lucky enough to have won a few awards for Zany Crazy code articles over the years

  • Microsoft C# MVP 2014
  • Codeproject MVP 2014
  • Microsoft C# MVP 2013
  • Codeproject MVP 2013
  • Microsoft C# MVP 2012
  • Codeproject MVP 2012
  • Microsoft C# MVP 2011
  • Codeproject MVP 2011
  • Microsoft C# MVP 2010
  • Codeproject MVP 2010
  • Microsoft C# MVP 2009
  • Codeproject MVP 2009
  • Microsoft C# MVP 2008
  • Codeproject MVP 2008
  • And numerous codeproject awards which you can see over at my blog

Comments and Discussions

 
GeneralMy vote of 5 Pinmemberfredatcodeproject22-May-12 2:35 
QuestionUtilities and nasty threading bug PinmemberNicolas Dorier28-Mar-12 22:33 
AnswerRe: Utilities and nasty threading bug PinmvpSacha Barber28-Mar-12 22:36 
GeneralRe: Utilities and nasty threading bug PinmemberNicolas Dorier28-Mar-12 22:37 
Utilities, the last code bit on this article
GeneralRe: Utilities and nasty threading bug PinmvpSacha Barber28-Mar-12 22:38 
GeneralRe: Utilities and nasty threading bug PinmemberNicolas Dorier28-Mar-12 22:42 
GeneralRe: Utilities and nasty threading bug PinmvpSacha Barber28-Mar-12 23:27 
GeneralRe: Utilities and nasty threading bug PinprotectorPete O'Hanlon29-Mar-12 1:34 
GeneralRe: Utilities and nasty threading bug Pinmemberbobfox8-Jan-13 14:16 
QuestionTag cloud PinmemberRichard Deeming26-Mar-12 9:37 
AnswerRe: Tag cloud PinmvpSacha Barber26-Mar-12 10:14 
AnswerRe: Tag cloud PinmemberNicolas Dorier28-Mar-12 22:26 
GeneralRe: Tag cloud PinmvpSacha Barber28-Mar-12 22:32 
GeneralRe: Tag cloud PinmemberNicolas Dorier28-Mar-12 22:34 
GeneralRe: Tag cloud PinmvpSacha Barber28-Mar-12 22:36 
QuestionAbsolutely fantastic! PinprotectorMarc Clifton26-Mar-12 7:36 
AnswerRe: Absolutely fantastic! PinmvpSacha Barber26-Mar-12 8:03 
GeneralRe: Absolutely fantastic! PinprotectorMarc Clifton26-Mar-12 11:21 
GeneralRe: Absolutely fantastic! PinprotectorPete O'Hanlon26-Mar-12 11:35 
GeneralRe: Absolutely fantastic! PinmvpSacha Barber27-Mar-12 5:10 
AnswerRe: Absolutely fantastic! PinmvpSacha Barber26-Mar-12 10:53 
GeneralRe: Absolutely fantastic! PinprotectorMarc Clifton26-Mar-12 11:09 
GeneralRe: Absolutely fantastic! PinprotectorPete O'Hanlon26-Mar-12 11:20 
GeneralRe: Absolutely fantastic! PinprotectorMarc Clifton26-Mar-12 11:22 
GeneralRe: Absolutely fantastic! PinprotectorPete O'Hanlon26-Mar-12 11:37 
AnswerRe: Absolutely fantastic! PinprotectorPete O'Hanlon28-May-12 10:21 
GeneralMy vote of 5 PinmemberSilvermanDK22-Mar-12 11:49 
GeneralRe: My vote of 5 PinmvpSacha Barber22-Mar-12 23:43 
GeneralMy vote of 5 Pinmemberpetervy21-Mar-12 18:48 
GeneralRe: My vote of 5 PinmvpSacha Barber21-Mar-12 20:41 
GeneralMy vote of 5 PinprotectorPete O'Hanlon21-Mar-12 10:25 
GeneralRe: My vote of 5 PinmvpSacha Barber21-Mar-12 11:05 
GeneralRe: My vote of 5 PinmemberSilvermanDK22-Mar-12 11:51 
GeneralMy vote of 5 Pinmembermaq_rohit21-Mar-12 8:21 
GeneralRe: My vote of 5 PinmvpSacha Barber21-Mar-12 8:31 
GeneralCongratz... PinmemberBlue_Boy21-Mar-12 4:43 
GeneralRe: Congratz... PinmvpSacha Barber21-Mar-12 4:54 
GeneralSacha missing... PinmvpNish Sivakumar21-Mar-12 4:42 
GeneralRe: Sacha missing... PinmvpSacha Barber21-Mar-12 4:54 
GeneralRe: Sacha missing... PinmvpNish Sivakumar21-Mar-12 4:56 
GeneralMy vote of 5 Pinmemberlinuxjr21-Mar-12 3:26 
GeneralRe: My vote of 5 PinmvpSacha Barber21-Mar-12 3:43 
GeneralMy vote of 5 PinmvpEspen Harlinn21-Mar-12 2:24 
GeneralRe: My vote of 5 PinmvpSacha Barber21-Mar-12 3:05 
QuestionREAD THIS : Broken links and formatting PinmvpSacha Barber21-Mar-12 2:01 
AnswerRe: READ THIS : Broken links and formatting PinmvpColin Eberhardt21-Mar-12 2:06 
GeneralRe: READ THIS : Broken links and formatting PinmvpSacha Barber21-Mar-12 3:05 
GeneralMy vote of 4 PinmemberUmesh. A Bhat21-Mar-12 1:57 
GeneralRe: My vote of 4 PinmvpSacha Barber21-Mar-12 2:00 
Suggestioncodstash.codeplex.com Pinmemberleppie21-Mar-12 1:38 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web02 | 2.8.141220.1 | Last Updated 21 Mar 2012
Article Copyright 2012 by Pete O'Hanlon, Sacha Barber
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid