Umbraco 7 Multi User Picker

Recently I had a chance to upgrade older web site based on Umbraco 6 to most recent version 7.2.2. Upgrade went more or less smooth as one would expect. Problem was upgrading third party components (when I say 3rd party i mean uComponents). I could live without uComponents but I also had custom Multi User Picker Data Type.

With Umbraco 7 you get User Picker Data type ( property editor) but that allows you to select just one User. I needed Data type that would give me list of CMS Users where I would be able select from (using checkboxes probably). At the time of writing there was no such components available for Umbraco 7.x so that meant building my own.

The whole idea behind these property editors in Umbraco is to simply show some values stored in data store to user in user friendly fashion and give them ability to edit those values. Before Umbraco version 7 it was possible to wrap different kinds of , let's call it code, into macro and use that as "property editor" for your data type. That was the time and version of Umbraco where those kinds of macros and property editors where in one way or another executed and effectively rendered on server side (XSLT, asp.net user controls etc). Umbraco version 7 was completely redesigned and everything that runs under your /umbraco/# part of the site was build as single page web application using AngularJS. This means if you want something injected and rendered in Umbraco back office you need to follow this pattern and build your Plugin (Data Type Property Editor) using AngularJS and available Umbraco Angular services and factories. Umbraco 7 documentation is not that great but at least they have basic introduction and some samples.    

Custom Property Editor in Umbraco 7.x

 First thing when build custom property editor is you need to create folder for it under App_Plugins folder. In my case it's called MultiUserPicker.

This folder will always consist of at least three files. Package manifest, describing your editor so that Umbraco knows how to deal with, AngularJS controller and some HTML file used as AngularJS template. For MultiUserPicker editor package.manifest is defined:
{   
    //you can define multiple editors   
    propertyEditors: [      
        {
            /*this must be a unique alias*/ 
            alias: "MultiUserPicker",
            /*the name*/
            name: "Multi User Picker",
            /*the html file we will load for the editor*/
            editor: {
                view: "~/App_Plugins/MultiUserPicker/MultiUserPickerEditor.html",
valueType: "JSON"
            }
        }
    ]
    ,
    //array of files we want to inject into the application on app_start
    javascript: [
        '~/App_Plugins/MultiUserPicker/MultiUserPicker.controller.js'
    ]
}

This is more or less self explanatory. Important in this case to notice is valueType: "JSON". This tells Umbraco what the internal data storage type for this field is. You can find more about this types here. You could use INT, STRING, TEXT, DATETIME or JSON as value type. View is path your editor HTML file and the last line tells where our AngularJS controller is.


Once package.manifest is defined you are already able to use it in Umbraco as custom data type. In Developer section expand Data types and click on Create new.


You should be able to select our new Multi User Picker editor from the drop down. 

Next we need a way to get Umbraco Users to our front end client side code. The way we do that is by creating specialized Umbraco WEB API controller.

namespace UmbracoProject.Controllers {
    public class UmbracoUser {
        public int Id { get; set; }
        public string Name { get; set; }
        public bool Selected { get; set; }
    }
    [PluginController("CustomEditors")]
    public class MultiUserPickerApiController : UmbracoAuthorizedJsonController {
        
        [HttpGet]
        public IEnumerable<UmbracoUser> GetAllUsers() {
            int totalUsers = 0;
            var allUsers = Services.UserService.GetAll(0, int.MaxValue, out totalUsers);
            var retVal = allUsers.Select(user => new UmbracoUser {
                Id = user.Id,
                Name = user.Name,
                Selected = false
            }).ToList();
            return retVal;
        }
    }
}

This controller exposes simple JSON API with one GetAllUsers method that returns list of all users in CMS. We will call this method (endpoint) from our AngularJS controller.

AngularJS controller code:

//adds the resource to umbraco.resources module:
angular.module('umbraco.resources').factory('multiUserPickerResource',
    function ($q, $http, $routeParams) {
        var apiCallsFactory = {};
        //the factory object returned
        //this cals the Api Controller
        apiCallsFactory.getAllUsers = function () {
            return $http.get("/umbraco/backoffice/CustomEditors/MultiUserPickerApi/GetAllUsers/");
        }
        return apiCallsFactory;
    }
);
angular.module("umbraco")
    .controller("multiUserPickerController", function ($scope, multiUserPickerResource) {
        // Fire when page is loaded and Plugin gets pulled in
        Reload();
        // Loads users from actual Umbraco page
        // Then calls Users API and adds missing users if one was added meanwhile
        function Reload() {
            //Bind Umbraco model to our local model
            $scope.Users = $scope.model.value;
            // Get Users from API
            var promiseGet = multiUserPickerResource.getAllUsers();
            promiseGet.then(function (pl) {
                var usersFromWebApi = pl.data;
                // Check if have some values comming from Actual Umbraco Fields value
                // if not just map all users from API to local model
                if ($scope.Users != "") {
                    angular.forEach(usersFromWebApi, function (usrFromApi, key1) {
                        var found = false;
                        angular.forEach($scope.Users, function (usrFromContent, key2) {
                            if (usrFromApi.Id == usrFromContent.Id) {
                                found = true;
                            }
                        });
                        if (!found) {
                            $scope.Users.push(usrFromApi);
                        }
                    });
                } else {
                    $scope.Users = usersFromWebApi;
                }
            },
            function (errorPl) {
                $log.error('failure loading Users from API', errorPl);
            });
        }
        // Map our local model to Umbraco page model to be autmaticaly saved to database
        $scope.SyncWithUmbracoModel = function (users) {
            $scope.model.value = users;
        };
    });

Basically what this code does is, on top of the listing I am registering my custom Angular factory that will be used for calling my custom .NET JSON API endpoint. Under the factory I am registering new controller within Angular application and passing in my factory as parameter (DI). Reload method is called on load, so each time page with our property editor loads we want to automatically show some values. When we defined our valueType in package.manifest as JSON we told umbraco to store values of this field in database as JSON (string) so anytime our controller gets instantiated we will get $scope object trough dependency injection. $scope.model.value is what holds value to be stored to database when the page is saved. When Reload method get triggered it assigns this value coming from Umbraco to my custom local Users model. This doesn't have to be this way necessarily, I could work only with $scope.model.value, but I found it more meaningful this way.     Reload method then calls our .NET endpoint and gets the list of all Umbraco users defined on system. It adds new Users to local model if some users were defined since last page save. Removing of the users is intentionally left out because Umbraco doesn't let you delete any users once defined. Last method from controller simply maps local model back to $scope.model.value to be saved to Umbraco database when page where editor is gets saved. 

Last piece of the puzzle is HTML template and it is defined as follows:

<div ng-controller="multiUserPickerController">
    <div>
        <label ng-repeat="usr in Users">
            <input type="checkbox" ng-model="usr.Selected" ng-click="SyncWithUmbracoModel(Users)" value="{{usr.Id}}" />
            {{usr.Name}}
        </label>
    </div>
</div>
What goes on in template is pretty straight forward Angular approach. Top element defines controller responsible for this context. Next we use ng-repeat to iterate trough all the users in $scope. ng-model on checkbox handles binding between control and the model and at last ng-click does the sync with Umbraco model to be saved to database.

Here is what end result looks like:

Here is the full solution if you need the code: 
UmbracoProject.zip (8.9MB)
Let me know if it works for you as it did for me.
blog comments powered by Disqus

About me

Bizic Bojan is Co-Founder of Amida IT-Services GmbH and Software Architect with focus on .NET, C++, Python and Cloud Native solutions. 

 

Disclaimer:

The opinions expressed herein are my own personal opinions and do not represent my employer’s view in any way.

Creative Commons License
This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported License.