2013 – My Umbraco Year in Review

Hello all,
As it’s the last day of 2013 I would like to look back and reflect on the year that has been.

It has been a great & fantastic year after a personal rocky year back in 2012, but as this year has progressed I have gained my confidence in myself back.

Continue reading “2013 – My Umbraco Year in Review”

Umbraco 7 – Creating a generic Settings editor with WebAPI & AngularJS

Hello all,
So my adventure for creating a new Umbraco 7 package for viewing Google Analytics information. I have created a generic way to edit settings from an XML file for this new Analytic section inside Umbraco.

So if I add any new elements to my XML config file, they will automatically appear in the Umbraco backoffice for it to be edited.

So I will run you through the parts such as the WebAPI Controller, and Angular to wire this all up nicely.

Firstly here is the XML config file I want to edit & if I add any new nodes it will automatically show in Umbraco to be edited:

<?xml version="1.0" encoding="utf-8" ?>
<Analytics>
  <ClientId label="Client ID" description="This is the Client ID key from the Google API">1234567</ClientId>
  <Username label="Username" description="The username for the Google account">warren</Username>
</Analytics>

So let’s take a look at the WebAPI controller I created to Get & Post the settings to and from the xml.config file.

using System.Collections.Generic;
using System.Linq;
using System.Web.Hosting;
using System.Xml;
using Analytics.Models;
using Umbraco.Web.Mvc;
using Umbraco.Web.WebApi;

namespace Analytics.Controllers
{
    /// <summary>
    /// Web API Controller for Fetching & Saving values from settings.config file
    /// 
    /// Example route URL to API
    /// http://localhost:62315/umbraco/Analytics/SettingsApi/GetSettings
    /// </summary>
    [PluginController("Analytics")]
    public class SettingsApiController : UmbracoAuthorizedApiController
    {
        /// <summary>
        /// Does what it says on the tin - the path to the settings.config file
        /// </summary>
        private const string ConfigPath = "~/App_Plugins/Analytics/settings.config";

        /// <summary>
        /// Gets Settings from the XML settings.config
        /// </summary>
        /// <returns>Serializes settings from XML file into a nice list of objects</returns>
        public List<SettingsValue> GetSettings()
        {
            //A list to store our settings
            var settings = new List<SettingsValue>();

            //Path to the settings.config file
            var configPath = HostingEnvironment.MapPath(ConfigPath);

            //Load settings.config XML file
            XmlDocument settingsXml = new XmlDocument();
            settingsXml.Load(configPath);

            //Get all child nodes of <Analytics> node
            XmlNodeList analayticsNode = settingsXml.SelectNodes("//Analytics");

            //Ensure we found the <Analytics> node in the config
            if (analayticsNode != null)
            {
                //Loop over child nodes inside <Analytics> node
                foreach (XmlNode settingNode in analayticsNode)
                {
                    foreach (XmlNode setting in settingNode.ChildNodes)
                    {
                        //Go & populate our model from each item in the XML file
                        var settingToAdd            = new SettingsValue();
                        settingToAdd.Key            = setting.Name;
                        settingToAdd.Label          = setting.Attributes["label"].Value;
                        settingToAdd.Description    = setting.Attributes["description"].Value;
                        settingToAdd.Value          = setting.InnerText;

                        //Add the item to the list
                        settings.Add(settingToAdd);
                    }
                }
            }

            //Return the list
            return settings;
        }

        public List<SettingsValue> PostSettings(List<SettingsValue> settings)
        {
            //Update the XML config file on disk

            //Path to the settings.config file
            var configPath = HostingEnvironment.MapPath(ConfigPath);

            //Load settings.config XML file
            XmlDocument settingsXml = new XmlDocument();
            settingsXml.Load(configPath);

            //Get all child nodes of <Analytics> node
            XmlNodeList analayticsNode = settingsXml.SelectNodes("//Analytics");

            //Loop over child nodes inside <Analytics> node
            foreach (XmlNode settingNode in analayticsNode)
            {
                foreach (XmlNode setting in settingNode.ChildNodes)
                {
                    //Go & populate our model from each item in the XML file
                    setting.InnerText = settings.SingleOrDefault(x => x.Key == setting.Name).Value;
                }
            }

            //Save the XML file back down to disk
            settingsXml.Save(configPath);

            //Return the same settings that passed in
            return settings;
        }
    }
}

You can see the controller is nicely commented and should be easy to follow along in what it is doing, you will notice I have a custom model/object to store the list of items so it can be serialised easily to JSON for the GET & POST requests. The model is as follows:

namespace Analytics.Models
{
    public class SettingsValue
    {
        public string Key { get; set; }

        public string Label { get; set; }

        public string Description { get; set; }

        public string Value { get; set; }
    }
}

So with these parts in place we have a way with a WebAPI to fetch & post values to our XML config file. Next we need to create an Angular View, Angular Controller & Angular Resource file to consume this WebAPI.

<div ng-controller="Analytics.SettingsController">
    <div class="umb-pane">
        <umb-control-group ng-repeat="setting in settings" label="{{ setting.Label }}" description="{{ setting.Description }}">
            <input type="text" name="{{ setting.Key }}" id="{{ setting.Key }}" value="{{ setting.Value }}" ng-model="setting.Value" />
        </umb-control-group>

        <div class="umb-tab-buttons">
            <div class="btn-group">
                <button ng-click="save(settings)" data-hotkey="ctrl+s" class="btn btn-success">
                    <localize key="buttons_save">Save</localize>
                </button>
            </div>
        </div>

    </div>
</div>

This Angular HTML view loops over the settings JSON object and repeats the markup & input field for each setting to edit within the XML file with the ng-repeat Angular data attribute.

The next part is to look at the Angular Controller file that is wired up to this view.

angular.module("umbraco").controller("Analytics.SettingsController",

function($scope, settingsResource, notificationsService) {

    //Get all settings via settingsResource - does WebAPI GET call
    settingsResource.getall().then(function(response) {
        $scope.settings = response.data;
    });

    //Save - click...
    $scope.save = function(settings) {

        //Save settings resource - does a WebAPI POST call
        settingsResource.save(settings).then(function(response) {
            $scope.settings = response.data;

            //Display Success message
            notificationsService.success("Success settings have been saved");
        });
    };

});

You can see we have created a Resource to get the settings and to save the settings, which the resource file handles the GET & POST to the WebAPI controller. When the save button is clicked it POSTs the settings to the WebAPI controller and shows a nice success message with the notification service.

angular.module("umbraco.resources")
    .factory("settingsResource", function($http) {
    return {

        getall: function() {
            return $http.get("Analytics/SettingsApi/GetSettings");
        },

        save: function(settings) {
            return $http.post("Analytics/SettingsApi/PostSettings", angular.toJson(settings));
        }
    };
}); 

With all these parts in place we now have a nice generic way of displaying & saving values to an XML config file and allowing easy update to the UI if we decide to add in more settings at a later date.

I hope someone finds this useful & can take something away to learn from it.

Cheers,
Warren 🙂

Quick Tip: Add a new custom section to Umbraco 7+

Hello all,
I have just come across a quick, simple & neat way to add a section to your Umbraco website/application be it for your own use or as a package developer.

So rather than talk about it for ages I will just jump straight into it.

    public class UmbracoStartup : ApplicationEventHandler
    {
        protected override void ApplicationStarted(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext)
        {
            //Get SectionService
            var sectionService = applicationContext.Services.SectionService;

            //Try & find a section with the alias of "mySection"
            var mySection = sectionService.GetSections().SingleOrDefault(x => x.Alias == "mySection");

            //If we can't find the section - doesn't exist
            if (mySection == null)
            {
                //So let's create it the section
                sectionService.MakeNew("My Section", "mySection ", "icon-users");
            }
        }
    }

So what this is doing on an application startup of Umbraco using the new Section Service checking to see if there a section with the alias of mySection already created, if not then using the section service it goes and creates it.

That’s it, it really is that simple.
Enjoy 🙂

Using Umbraco Pipeline for Member Profile URLs

Hello all,
I wanted to share with you all a new approach I have learnt after reading up on the Umbraco Pipeline.

I originally asked the question over on our.umbraco.org on how best to achieve this and first came up with URL Rewriting to solve my solution, then the thread evolved and I was able to use a custom controller & route, but now I have found a much simpler & elegant way to deal with it, which is to use an iContentFinder class.

First lets start off with creating an custom iContentFinder which is used in the pipeline for trying to find an Umbraco node that matches the URL, if it doesn’t find it it gets passed onto the next iContentFinder and so on until it gets to the bottom which is the 404 iContentFinder.

using System;
using System.Linq;
using System.Web;
using umbraco.cms.businesslogic.member;
using Umbraco.Core;
using Umbraco.Web.Routing;

namespace MySite.BusinessLogic
{
    public class MemberProfileContentFinder : IContentFinder
    {
        public bool TryFindContent(PublishedContentRequest contentRequest)
        {
            var urlParts = contentRequest.Uri.GetAbsolutePathDecoded().Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
            
            //Check if the Url Parts
            // Starts with /member/*
            if (urlParts.Length > 1 && urlParts[0].ToLower() == "member")
            {
                //Lets try & find the member
                var memberName = urlParts[1];

                //Try and find a member where the property matches the memberName
                var tryFindMember = Member.GetAll.SingleOrDefault(x => x.getProperty("profileURL").Value.ToString() == memberName);

                //Need to set the member ID or pass member object to published content
                //Will pass a null to view if not found & can display not found member message
                HttpContext.Current.Items["memberProfile"] = tryFindMember;

                //Add in string to items - for profile name user was looking for
                HttpContext.Current.Items["memberName"] = memberName;

                //Set the Published Content Node to be the /Profile node - can get properties off it & my member profile in the view
                contentRequest.PublishedContent = contentRequest.RoutingContext.UmbracoContext.ContentCache.GetByRoute("/view-profile");

                //Return true to say found something & stop pipeline & other contentFinder's from running
                return true;
            }

            //Not found any content node to display/match - so run next ContentFinder in Pipeline
            return false;
        }
    }
}

In this code you can see it is quite straight forward. It is getting the URL from the request and splitting the string on the URL parts such as http://mysite.co.uk/member/john into a string array so the variable urlParts will have two items in the array which are member and john.

We then next check that we have at least one URL part and that the first URL part in our array is equal to members, as we don’t want to run our logic if it does not match this URL fragment.

Next part is for us to get the second item in the array which is the member profile URL we want to look up. Using the Umbraco Member API we fetch all members and using a SingleOrDefault() Linq query we check to see if we can find a single member where the property memberProfileURL on the member matches that to our variable which contains the member URL to lookup.

Moving on we add the found member (even if it returns null, aka not found a member that has that profile URL) & the member profile URL to the HTTPContext Items so that we can retrieve them from our view.

The magic part of this is the following line:


//Set the Published Content Node to be the /Profile node - can get properties off it & my member profile in the view
contentRequest.PublishedContent = contentRequest.RoutingContext.UmbracoContext.ContentCache.GetByRoute("/view-profile");

By setting PublishedContent on the contentRequest object, we can choose what node in the Umbraco content tree we want to render. In our case I am using the routing context to help get me the node based from it’s URL, so in this case I am fetching the node that matches the URL, /view-profile

The final part is that we return true, so that we let the pipeline know that we have a found a node and to stop running any further iContentFinders from being processed. By doing so we will load the view profile node and it’s template.

The next part of this is to now register our content finder in the application starting event with this lovely code snippet so it can run in our site.

using Umbraco.Core;
using Umbraco.Core.Persistence;
using Umbraco.Web.Routing;

namespace MySite.BusinessLogic
{
    public class UmbracoStartup : ApplicationEventHandler
    {
        protected override void ApplicationStarting(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext)
        {
            // Insert my MemberProfileContentFinder before ContentFinderByNiceUrl
            ContentFinderResolver.Current.InsertTypeBefore<ContentFinderByNiceUrl, MemberProfileContentFinder>();
        }
    }
}

Now that we have our custom iContentFinder registered by Umbraco, we just need to ensure our template/view for our view profile node uses the values in the HTTPContext.Items collection that we set inside the iContentFinder.

@using Member = umbraco.cms.businesslogic.member.Member
@inherits Umbraco.Web.Mvc.UmbracoTemplatePage
@{
    Layout = "Master.cshtml";

    //Get the values out of the HTTPContext that we set in the iContentFinder
    var memberProfile   = (Member)HttpContext.Current.Items["memberProfile"];
    var memberName      = (string)HttpContext.Current.Items["memberName"];

    //Bit of string replacment on the noUserFound property on the View Profile node
    //Replace it with the member name the user was trying to look for
    var notFoundMessage = Model.Content.GetProperty("noUserFound").Value.ToString();
    notFoundMessage     = notFoundMessage.Replace("##user##", memberName);
}

<h2>View Profile</h2>

@if (memberProfile == null)
{
    //Display User Not Found Message
    //Do string replacement on copy ##user## with memberName
    @Html.Raw(notFoundMessage)
}
else
{
    //We found the member - so pass the member object to our partial
    //To display the member's details etc 🙂
    @Html.Partial("MemberProfile", memberProfile)
}

In this template/view you can see I am fetching the items out of the HTTPContext and casting it to a string for the profile URL name and the other as an Umbraco Member object so we can get intellisense.

The next part is that I am doing some string replacement on an field that I am fetching off the View Profile node for a user not found message.

I then simply display the not found message if the member is null, because the iContentFinder did not set a value for it. Otherwise we pass the member into our partial view where we can display specific information about our member, like member name, email, profile image & any other custom fields set on the member type.

So the final part of the puzzle is the Partial View

@inherits Umbraco.Web.Mvc.UmbracoViewPage <umbraco.cms.businesslogic.member.Member>
<h2>Member Profile Partial</h2>

<h3>Name: @Model.Text</h3>
<h3>Email: @Model.Email</h3>
<h3>Custom Field: @Model.getProperty("customField").Value</h3>

The only important thing to know here is that the first line is that I am setting the object type to be an Umbraco Member, which lets the model know its an Umbraco Member and gives us intellisense in Visual Studio when writing our partial view to display public information about our member.

That’s it, it’s really as simple as that.

Thanks to the Umbraco community & Stephane Gay for some help with the iContentFinder Pipeline stuff.