How GitHub Snippets for Umbraco 7 was built

Heads Up!

This article is several years old now, and much has happened since then, so please keep that in mind while reading it.

Hello all,
So for today's advent calendar surprise I will be writing about how I recently built the GitHub Snippets for Umbraco 7 package and what issues or problems I came across and learnt along the way.

So first things first, let's get a bit familiar with the package itself.

What are we building?

This package allows you to point to a GitHub repository or use the default repository that comes with the package to allow you to insert Razor snippets into the Partial view or Template Editor in Umbraco. It's as simple as that.

Let's take a quick look of what it looks like in action:

Creating the Button

Here you can see the new button we have added to the Umbraco Backoffice
Here you can see the new button we have added to the Umbraco Backoffice

The first step of this is creating an additional button in the Umbraco backoffice called Snippets which when clicked will show us our new dialog only on the Partial View or Template Editor page/s.

So how do we go about creating this new button? It's simpler than you think, let's look at the code snippet and then step through it.

 

using System;
using System.Web.UI;
using System.Web.UI.WebControls;
using Umbraco.Core;
using umbraco.presentation.masterpages;
using umbraco.uicontrols;

namespace GitHubSnippets
{
    public class StartupHandlers : IApplicationEventHandler
    {
        public void OnApplicationInitialized(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext)
        {
            //throw new NotImplementedException();
        }

        public void OnApplicationStarting(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext)
        {
            //throw new NotImplementedException();
        }

        
        public void OnApplicationStarted(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext)
        {
            //When Umbraco Has Started Up...
            //Hook into the UmbracoPage Load event - which is any Umbraco backoffice page
            umbracoPage.Load += umbracoPage_Load;
        }


        void umbracoPage_Load(object sender, EventArgs e)
        {
            //Cast sender as an Umbraco Page object
            var pageReference = (umbracoPage)sender;

            //Get the path of the current page
            var path = pageReference.Page.Request.Path.ToLower();

            //Check if the path of the page ends in either of the following...
            if (path.EndsWith("settings/views/editview.aspx") == true || path.EndsWith("settings/edittemplate.aspx"))
            {
                //Try & get body panel control from the page (as still .NET page)
                var c2 = GetPanel1Control(pageReference);

                //If we have found it then...
                if (c2 != null)
                {
                    //Cast the control we found as an Umbraco Panel object
                    var panel = (UmbracoPanel)c2;

                    //This is the Javascript we want to run when our button is clicked
                    //Including what happens when the dialog closes & invokes our callback
                    var javascript = @"UmbClientMgr.openAngularModalWindow({
                                        template: '/app_plugins/snippets/snippet-dialog.html',
                                        callback: function(data) {
                                            var snippet = JSON.parse(data.code);
                                            UmbClientMgr.contentFrame().UmbEditor.Insert(snippet, '');
                                            top.UmbSpeechBubble.ShowMessage('success', 'Snippet Inserted', 'Yipee you have sucessfully inserted a snippet from GitHub called: ' + data.name);
                                        }
                                    });";
                    
                    //Lets create a new button and add it to the panel control
                    var snippetBtn              = panel.Menu.NewButton(-1);
                    snippetBtn.Text             = "Insert Snippet";
                    snippetBtn.ToolTip          = "Insert Snippet";
                    snippetBtn.ButtonType       = MenuButtonType.Primary;
                    snippetBtn.Icon             = "code";
                    snippetBtn.OnClientClick    = javascript;

                }
            }

        }


        //Get the body panel control
        private Control GetPanel1Control(umbracoPage up)
        {
            var cph = (ContentPlaceHolder)up.FindControl("body");

            return cph.FindControl("body_Panel1_container");
        }
    }
}

This snippet allows you to add a button to the Umbraco backoffice

So the snippet above will add a button to the Umbraco backoffice using the UmbracoPage Load event and check if the page URL is the template of partial view editor, if so it will add our Add Snippet button.

As you can see the button has some inline JavaScript that is run when it is clicked. This was the hardest part for me and involved me trawling through the Umbraco source on GitHub and asking lots of questions to lead Umbraco Belle developer Per Ploug on twitter.

But the most important part from the JavaScript is the UmbClientMgr.openAngularModalWindow() function that has a path to our Angular HTML file for the modal and a callback function that is invoked when the modal is posted back or a selection has been made, in our case a snippet has been selected.

Inside the callback you will see two further calls that took me a few hours to find, but allows me to insert the snippet into the code editor and then one to display a nice friendly notification message.

UmbClientMgr.contentFrame().UmbEditor.Insert() is the function that allows me to inser the code snippet and the top.UmbSpeechBubble.ShowMessage() allows me to show a nice new notification. Some of these calls are legacy but allows a WebForms page like the template editor to run the new Angular Services like NotificationService by using an old function such as UmbSpeechBubble.ShowMessage().

Creating the WebAPI Controller to fetch GitHub Snippets

So now onto the next part and look at how I built the WebAPI that the Angular Modal uses to go and fetch the snippets from the GitHub repository using GitHub's API. Lets dive straight into the Controller code and review it afterwards.

using Umbraco.Web.Mvc;
using Umbraco.Web.WebApi;

namespace GitHubSnippets.Controllers
{
    // Web API Calls can be access from this URL
    // /umbraco/snippets/*MethodName*
    [PluginController("Snippets")]
    public class GitHubController : UmbracoAuthorizedApiController
    {
        private static string _baseAPIUrl = "https://api.github.com";

        /// <summary>
        /// Get's the respository user from config file
        /// </summary>
        public string GetRepositoryUser()
        {
            //Returns a value from our settings config file
            return Settings.GetSetting("RepositoryUser");
        }

        /// <summary>
        /// Get's the respository name from config file
        /// </summary>
        /// <returns></returns>
        public string GetRepositoryName()
        {
            //Returns a value from our settings config file
            return Settings.GetSetting("RepositoryName");
        }

        /// <summary>
        /// Fetch contents of a specific file or folder from a GitHub Repo
        /// </summary>
        /// <param name="path">The path to the file or folder to request</param>
        public async Task<JToken> GetContent(string path)
        {
            //Get path from parameter
            //If the path length is greater than 1 AND it starts with a /
            if (path.Length > 1 && path.StartsWith("/"))
            {
                //Lets remove the starting /
                path = path.TrimStart('/');
            }

            //Get Settings from config file
            var repoUser    = Settings.GetSetting("RepositoryUser");
            var repo        = Settings.GetSetting("RepositoryName");

            //Format API Url to request
            var apiUrl = string.Format("{0}/repos/{1}/{2}/contents/{3}", _baseAPIUrl, repoUser, repo, path);

            //Do an async call to the GitHub API
            HttpClient client               = new HttpClient();
            HttpResponseMessage response    = await client.GetAsync(apiUrl);

            //If not success code throw a 404 not found
            if (!response.IsSuccessStatusCode)
            {
                throw new HttpResponseException(HttpStatusCode.NotFound); 
            }

            //The remote JSON we recieve - gets it as a string
            //Need to convert it to a JSON object
            var content = await response.Content.ReadAsAsync<JToken>();

            //Return the JSON
            return content;
        }

        /// <summary>
        /// Fetch decoded contents of a specific file from a GitHub Repo
        /// </summary>
        /// <param name="path">The path to the file or folder to request</param>
        public async Task<string> GetContentDecoded(string path)
        {
            //Get path from parameter
            //If the path length is greater than 1 AND it starts with a /
            if (path.Length > 1 && path.StartsWith("/"))
            {
                //Lets remove the starting /
                path = path.TrimStart('/');
            }

            //Get Settings from config file
            var repoUser    = Settings.GetSetting("RepositoryUser");
            var repo        = Settings.GetSetting("RepositoryName");

            //Format API Url to request
            var apiUrl = string.Format("{0}/repos/{1}/{2}/contents/{3}", _baseAPIUrl, repoUser, repo, path);

            //Do an async call to the GitHub API
            HttpClient client = new HttpClient();
            HttpResponseMessage response = await client.GetAsync(apiUrl);

            //If not success code throw a 404 not found
            if (!response.IsSuccessStatusCode)
            {
                throw new HttpResponseException(HttpStatusCode.NotFound);
            }

            //The remote JSON we recieve - gets it as a string need to return a nice JSON object
            var content = await response.Content.ReadAsAsync<JToken>();

            //Decode the base64 content of the file using the helper
            var decodedContent = base64Decode(content["content"].ToString());

            return decodedContent;
        }

        /// <summary>
        /// http://forums.asp.net/t/645898.aspx
        /// </summary>
        /// <param name="data">The original base64 string to decode</param>
        /// <returns>Decoded string</returns>
        internal string base64Decode(string data)
        {
            try
            {
                System.Text.UTF8Encoding encoder    = new System.Text.UTF8Encoding();
                System.Text.Decoder utf8Decode      = encoder.GetDecoder();

                byte[] todecode_byte    = Convert.FromBase64String(data);
                int charCount           = utf8Decode.GetCharCount(todecode_byte, 0, todecode_byte.Length);
                char[] decoded_char     = new char[charCount];
                utf8Decode.GetChars(todecode_byte, 0, todecode_byte.Length, decoded_char, 0);

                string result = new String(decoded_char);
                return result;
            }
            catch (Exception e)
            {
                throw new Exception("Error in base64Decode" + e.Message);
            }
        }
    }
}

The points to take away from this code snippet is that firstly I have inherited my class from UmbracoAuthorizedApiController this allows only users logged into the Umbraco back office to make a call to this WebAPI, as we don't just want anyone to be able to fetch our GitHub Snippets publically. So inheriting from this class allows Umbraco to deal with the authentication for us.

With that part done the next thing to know is that I have applied an attribute [PluginController("Snippets")]. What this does allows Umbraco to detect in our DLL once this is built, that this a controller that needs to be registered and automatically routed for us. So all of my WebAPI calls can be done by going to http://site.co.uk/umbraco/snippets/methodName

The final part to learn from this code snippet GetContent() and GetDecodedContent() call the remote GitHub API to go and fetch the remote JSON from their API. I won't go into too much detail here as hopefully the code is commented enough for you to understand what is happening. But in a nutshell you just need to know it's getting JSON from GitHub's API and returning it for us.

Creating the Modal HTML View & Angular Components

Here you can see the new dialog listing out GitHub snippets inside it
Here you can see the new dialog listing out GitHub snippets inside it

This next step involves a few components. It first includes the HTML for the modal with Angular directives such as ng-repeat to loop through our list of Snippets from the WebAPI call we make, along with an Angular resource and controller JavaScript files and finally a JSON manifest file to get our JavaScript files picked up by the Umbraco back office.

Lets start off with the Angular Resource JavaScript file, as this file goes and fetches data from our WebAPI we have just created.

angular.module("umbraco.resources")
    .factory("snippetResource", function ($http) {
        return {
            getSnippets: function (path) {
                return $http.get("/umbraco/snippets/github/GetContent?path=" + path);
            },
            
            getSnippetDecoded: function (path) {
                return $http.get("/umbraco/snippets/github/GetContentDecoded?path=" + path);
            },

            getRepoUser: function () {
                return $http.get("/umbraco/snippets/github/GetRepositoryUser");
            },

            getRepoName: function () {
                return $http.get("/umbraco/snippets/github/GetRepositoryName");
            }
        };
});

In this Angular Resource file you can see I have a few functions such as getSnippets, getSnippetDecoded which uses Angulars $http helper to fetch the remote JSON from our WebAPI we created.

Additionally you can see that I have given it a name of snippetResource so that it can be used in our Angular Controller file.

The next part of the puzzle is the Angular Controller needed to pass data to our HTML view for the modal.

angular.module("umbraco").controller("Snippets.GitHubController", function ($scope, snippetResource) {

    //Repository Name & User
    $scope.repoUser = snippetResource.getRepoUser();
    $scope.repo     = snippetResource.getRepoName();

    //Get Snippets from Resource (API)
    snippetResource.getSnippets('/').then(function (snippets) {
        $scope.snippets = snippets;
    });
    
    //Insert Snippet - button click
    $scope.insertSnippet = function (selectedSnippet) {
        
        var selectedSnippetPath = selectedSnippet.path;

        //Get the snippet to decode from the Resource aka API
        snippetResource.getSnippetDecoded(selectedSnippetPath).then(function (snip) {
            
            //Create a snippet object to pass through to callback
            var snippet = {
                name: selectedSnippet.name,
                code: snip.data
            };

            //Debugging
            console.log(snippet);

            //Submit dialog - fires callback event for open dialog
            $scope.submit(snippet);
        });

    };
    
});

The first part is that we need to register our controller in the Umbraco module or app as its also known as in Angular. The controller name Snippets.GitHubController is what we will need to use in our HTML view for our modal.

The next part is that I am registering the snippetResource in the controller initialisation, so that I can use it in the controller to talk to our WebAPI controller to retrieve JSON. The first two lines is getting the repository name and repository user from our WebAPI via the snippetResource and defining them to properties on the $scope so we can use them easily in our view.

We are using the snippetResource to then fetch all the snippets and wait until it's returned to then assign the JSON it recieves from the WebAPI to a property called snippets on the $scope. So that we can easily loop & iterate over it in our modal view.

The final part of this controller is to create a function called insertSnippet that takes the selected snippet JSON object and then call the API to get the decoded contents of the selected snippet from our Web API.

I then create a new JSON object and assign the snippet contents and the snippet name and pass that to a function called $scope.Submit() which closes the dialog and will then invoke the callback function we created for when our main insert snippet button was clicked. So this will allow the snippet JSON object to be used in the callback for it to be inserted into the code editor and the notification to be shown.

Next is to create our modal HTML view and wire it up to our Angular controller.

<div class="umb-panel" ng-controller="Snippets.GitHubController">
    <div class="umb-panel-footer">
        <div class="btn-toolbar umb-btn-toolbar pull-right">
            <a href class="btn btn-link" ng-click="close()">
                <localize key="cancel" />
            </a>
        </div>
    </div>
    
    <div class="umb-modalcolumn-header">
        <div class="umb-el-wrap umb-panel-buttons">
            <div class="span6">
                <div class="form-search">
                    <i class="icon-search"></i>
                    <input type="text" ng-model="searchTerm" class="umb-search-field search-query" placeholder="Filter...">
                </div>
            </div>
        </div>
    </div>
    
    <div class="umb-panel-body umb-scrollable">
        <div class="tab-content umb-control-group">
            <div class="umb-pane">
                <h4>GitHub Snippets</h4>
                <h5>{{ repoUser.data }}/{{ repo.data }}</h5>

                <ul class="unstyled">
                    <li ng-repeat="snippet in snippets.data | filter: searchTerm" class="clearfix">
                        <span class="pull-left">{{ snippet.name }}</span> 
                        <a href="#" class="btn btn-primary pull-right" ng-click="insertSnippet(snippet)" prevent-default>
                            <i class="icon icon-code"></i> Insert Snippet
                        </a>
                    </li>
                </ul>
            </div>
        </div>
    </div>
</div>



This is the HTML for the modal view, I had to take a look at a few modals in the Umbraco source code to see what markup and CSS classes was expected for it to look like a neat modal and to fit in with the rest of the Umbraco Belle user interface.

The points to take away from this is that the first div has an attribute called ng-controller with the value of Snippets.GitHubController which is the name of our controller we defined in the previous file. I won't go into too much depth & detail here and this is relatively basic Angular View that is fetching & displaying values from the $scope in our controller and iterating over a loop for our snippets property on the $scope.

You can see inside my loop of the snippets is that I have a button that has an attribute of ng-click with the value of insertSnippet(snippet) that calls our function in our controller. Which goes and fetches the content of the selected snippet via the API and then closes the dialog, inserts the snippet & shows the notificiation.

We are almost on the home straight now, the final piece of the jigsaw puzzle is to create a package.manifest JSON file in order to register our two Javascript files into the Umbraco application.

{
        propertyEditors: [],

        javascript: [
            '~/App_Plugins/Snippets/Snippets.Github.Controller.js',
            '~/App_Plugins/Snippets/Snippet.Resource.js'
        ]
}

The one thing to note is that our JavaScript & HTML modal needs to live in Umbraco's plugin folder which is /App_Plugins in there I have created a folder called Snippets where the following files need to live:

  • /App_Plugins/Snippets/Snippet.Resource.js
  • /App_Plugins/Snippets/Snippets.GitHub.Controller.js
  • /App_Plugins/Snippets/snippet-dialog.html
  • /App_Plugins/Snippets/package.manifest

With these files in place and our code compiled into a DLL so the button in our event handler and our WebAPI we are good to go to test this out.

 

Final Conclusion

Here you can see the finished snippet inserted into the editor & with a nice neat notification
Here you can see the finished snippet inserted into the editor & with a nice neat notification

So now we are all done lets take a look at the finished thing, it's a thing of beauty! Thank you for taking the time out to read this massive post but I hope you have found some useful snippets or pointers for yourself to take away and use in your own projects.

I hope you found today's Umbraco advent calendar was helpful and useful to you all. I look forward to seeing what all you happy Umbraco developers create with Umbraco 7

Well there's only one thing left to say, Happy Christmas & New Year.
Warren x

 

Warren Buckley

Warren is on Twitter as