Nibble

Extending the Umbraco Backend Using MVC

1 thing I wanted to do while creating Optimus for Umbraco was creating the page that is responsible for editing the bundle use the MVC framework instead of Webforms

The backoffice of Umbraco v4/v6 is still Webforms but it’s perfectly possible to plug in pages that use MVC.

The sourcecode for Optimus is available and you can use that to figure out how it’s done but I thought It would be even easier if I created a starter project for that Smile

Umbraco MVC Backoffice Pages

So I’ve just published a new project on github that does exactly that
https://github.com/TimGeyssens/UmbracoMVCBackofficePages

You might have come accros this post by Bart de Meyer http://blog.bartdemeyer.be/2013/01/using-mvc-backend-pages-in-umbraco-4-11-1/ that also talks about this subject but there is a difference in the approach I’m using since I’m not making use of a SurfaceController (since those are for frontend use) and I’m also trying to keep things seperated by working in the /App_plugins folder (~\App_Plugins\UmbracoMVCBackofficePages\ folder in this case)

Solution setup

image

The vs solution consist of 2 projects, 1 test site that just contains Umbraco and the the project where we’ll add our code and push to the test site using post build events.

The post build events (moving the assembly and the views/icons used)

xcopy "$(ProjectDir)bin\UmbracoMVCBackofficePages.*" "$(ProjectDir)..\TestSite\bin\" /Y
 
xcopy "$(ProjectDir)Icons\*.*" "$(ProjectDir)..\TestSite\App_Plugins\UmbracoMVCBackofficePages\Icons\" /Y   
 
xcopy "$(ProjectDir)Views\*.*" "$(ProjectDir)..\TestSite\App_Plugins\UmbracoMVCBackofficePages\Views\" /Y 

Step 1: Data

So first step it to create some data that we’ll be working with as this is an example I just creates a very simple Person class and another class that will return some sample data

Person class:

 public class Person
    {
        public Person(int id, string firstName, string lastName)
        {
            Id = id;
            FirstName = firstName;
            LastName = lastName;
 
        }
        public int Id { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
 
        public override string ToString()
        {
            return FirstName + " " + LastName;
        }
    }

 

Data class:

 public class Data
    {
        public static IEnumerable<Person> GetAll()
        {
            var data = new List<Person>();
            data.Add(new Person(1, "Jeff", "Trent"));
            data.Add(new Person(2, "Paula", "Trent"));
            data.Add(new Person(3, "Lieutenant", "Harper"));
            data.Add(new Person(4, "Colonel", "Edwards"));
            data.Add(new Person(5, "Patrolman", "Larry"));
            return data;
        }
 
        public static Person GetById(int id)
        {
 
            return GetAll().Where(p => p.Id == id).FirstOrDefault();
        }
    }


So imagine that this interacts with your datasource….

Step 2: The Tree

Next I’ll add a tree to the settings section that will list my data

 [Tree("settings", "exampleTree", "Example")]
    public class ExampleTree : BaseTree
    {
        public ExampleTree(string application)
            : base(application)
        {
        }
 
        protected override void CreateRootNode(ref XmlTreeNode rootNode)
        {
            rootNode.NodeType   = "example";
            rootNode.NodeID     = "init";
            rootNode.Menu =  new List<IAction> { ActionRefresh.Instance };
        }
 
        public override void Render(ref XmlTree tree)
        {
 
            foreach (var person in Data.GetAll())
            {
                var node = XmlTreeNode.Create(this);
                node.NodeID = person.Id.ToString();
                node.NodeType = "person";
                node.Text = person.ToString();
                node.Action = string.Format("javascript:openExamplePage({0});",
                    person.Id.ToString());
                node.Icon = "../../../App_Plugins/UmbracoMVCBackofficePages/Icons/example-icon.png";
                node.OpenIcon = "../../../App_Plugins/UmbracoMVCBackofficePages/Icons/example-icon.png";
                node.HasChildren = false;
                node.Menu = new List<IAction>();
                OnBeforeNodeRender(ref tree, ref node, EventArgs.Empty);
                if (node != null)
                {
                    tree.Add(node);
                    OnAfterNodeRender(ref tree, ref node, EventArgs.Empty);
                }
 
            }
 
        }
 
        public override void RenderJS(ref System.Text.StringBuilder Javascript)
        {
            Javascript.Append(
               @"function openExamplePage(id) {
                 UmbClientMgr.contentFrame('../App_Plugins/UmbracoMVCBackofficePages/Index?id='+id);
                }");
        }
    }

 

Important part here is the js function that will open the edit page and pass it the id (check the node action)

UmbClientMgr.contentFrame(’../App_Plugins/UmbracoMVCBackofficePages/Index?id=’+id);

Of course we’ll need to make sure ../App_Plugins/UmbracoMVCBackofficePages/Index get’s routed correctly (more in the next steps)

Step 3: Routing

Since I’m not making use of an Umbraco SurfaceController I need to take care of the routing

So my route config looks like

 public class RouteConfig
    {
        public static void RegisterRoutes(RouteCollection routes)
        {
            routes.MapRoute(
               name: "ExampleMVCBackofficePages",
               url: "App_Plugins/UmbracoMVCBackofficePages/{action}/{id}",
               defaults: new { controller = "Example", action = "Index", id = UrlParameter.Optional }
           );
        }
    }

And to make sure that is done when the application is started I have an Umbraco ApplicationEventHandler (since I’m using 6.1 +) in place

public class StartUpHandlers : ApplicationEventHandler
    {
        protected override void ApplicationStarted(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext)
        {
            RouteConfig.RegisterRoutes(RouteTable.Routes);
        }
    }

 

Step 4: VIew model

Pretty simple model making use of data annotations

public class PersonViewModel
    {
        public int Id { get; set; }
 
        [Display(Name = "First name:")]
        [Required]
        public string FirstName { get; set; }
 
        [Display(Name = "Last name:")]
        [Required]
        public string LastName { get; set; }
 
    }

 

Step 5: Controller

An  MVC controller inheriting from UmbracoAuthorizedController that takes care of the authentication (so you can’t access App_Plugins/UmbracoMVCBackofficePages/Index if you aren’t logged into the backoffice)

public class ExampleController : UmbracoAuthorizedController
    {
        public ActionResult Index(int id)
        {
            var p = Data.GetById(id);
 
            PersonViewModel model = new PersonViewModel();
            model.Id = p.Id;
            model.FirstName = p.FirstName;
            model.LastName = p.LastName;
 
            return View("~/App_Plugins/UmbracoMVCBackofficePages/Views/Index.cshtml", model);
        }
 
        [HttpPost]
        public ActionResult Edit(PersonViewModel person)
        {
            if (ModelState.IsValid) { }
                //do something
 
            TempData["success"] = true;
 
            return View("~/App_Plugins/UmbracoMVCBackofficePages/Views/Index.cshtml",person);
        }
    }

But notice that I do specify the view path since it won’t look in that directory by default

Step 6: View

Strongly typed view that uses the same js/css/markup as the backoffice webforms pages so it also has the same look Smile (and also shows the speech bubble when the form was submitted successfully)

@model UmbracoMVCBackofficePages.Models.PersonViewModel
 
<!doctype html>
<html>
<head>
    <title>Example editor</title>
   
    <script src="~/umbraco_client/ui/jquery.js" type="text/javascript"></script>
    <script src="~/umbraco_client/Application/NamespaceManager.js" type="text/javascript"></script>
    <script src="~/umbraco_client/Application/UmbracoApplicationActions.js" type="text/javascript"></script>
    <script src="~/umbraco_client/Application/UmbracoUtils.js" type="text/javascript"></script>
    <script src="~/umbraco_client/Application/UmbracoClientManager.js" type="text/javascript"></script>
    <script src="~/umbraco_client/ui/default.js" type="text/javascript"></script>
 
 
    <link href="~/umbraco_client/ui/default.css" rel="stylesheet" />
    <link href="~/umbraco_client/menuicon/style.css" rel="stylesheet" />
    <link href="~/umbraco_client/panel/style.css" rel="stylesheet" />
    <link href="~/umbraco_client/propertypane/style.css" rel="stylesheet" />
    <link href="~/umbraco_client/scrollingmenu/style.css" rel="stylesheet" />
    
  
 
    <style>
 
        #save {
            height: 26px;
            margin: 0;
            padding: 0;
        }
 
        #save img {
            padding: 0;
            margin: 0;
        }   
    </style>
    @if (TempData["success"] != null)
    {
        <script>
            UmbClientMgr.mainWindow().UmbSpeechBubble.ShowMessage(’save’, ‘Saved’, ’successfully saved’);
        </script>
    }
</head>
    <body>
        
        @using (Html.BeginForm("Edit", "Example"))
        {
        <div id="body_UmbracoPanel" class="panel" style="width:100%;">
            <div class="boxhead">
                <h2 id="body_UmbracoPanelLabel">Example Editor</h2>
            </div>
            <div class="boxbody">
                <div id="body_UmbracoPanel_menubackground" class="menubar_panel">
                    <span id="body_UmbracoPanel_menu">
                        <table id="body_UmbracoPanel_menu_tableContainer">
                            <tbody>
                                <tr id="body_UmbracoPanel_menu_tableContainerRow">
                                    <td id="body_UmbracoPanel_menu_tableContainerButtons">
                                        <button type="submit" id="save">
                                            <img src="~/umbraco/images/editor/save.gif" alt="Save Bundle" class="editorIcon"/>
                                        </button>
                                    </td>
                                </tr>
                            </tbody>
                        </table>
                    </span>
                </div>
                <div id="body_UmbracoPanel_content" class="content">
                    <div class="innerContent" id="body_UmbracoPanel_innerContent">
                        <h2 class="propertypaneTitel">Details</h2>
 
                        @Html.HiddenFor( m => m.Id)
 
                        <div class="propertypane">
                            <div>
                                <div class="propertyItem">
                                    <div class="propertyItemheader">@Html.LabelFor(m => m.FirstName)</div>
                                    <div class="propertyItemContent">
                                        @Html.EditorFor(m => m.FirstName)
                                        @Html.ValidationMessageFor(m => m.FirstName)
                                    </div>
                                </div>
 
                                <div class="propertyItem">
                                    <div class="propertyItemheader">@Html.LabelFor(m => m.LastName)</div>
                                    <div class="propertyItemContent">
                                        @Html.EditorFor(m => m.LastName)
                                         @Html.ValidationMessageFor(m => m.LastName)
                                    </div>
                                </div>
 
                                <div class="propertyPaneFooter">-</div>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
            <div class="boxfooter">
                <div class="statusBar">
                    <h2></h2>
                </div>
            </div>
        </div>
        }
    </body>
</html>

Result

So that are the different bits and the end result should look like

image

Again source code for this project is available at https://github.com/TimGeyssens/UmbracoMVCBackofficePages

(I’ll also do a follow up post to show you how to get create/delete working)

21 Comments so far

  1. Adrian on July 18th, 2013

    Hi
    This is a really great post, but you use Html.BeginForm which gives me a ‘System.Web.Mvc.HtmlHelper’ does not contain a definition for ‘BeginForm’ and no extension method ‘BeginForm’ error when deploying to Umbraco 6.1.x

    Any tips would be most welcome

  2. Tim Geyssens on July 18th, 2013

    @Adrian, have you tried downloading and running my example site, does that run?

    Try adding @using System.Web.Mvc.Html

    Or make sure you have the same web.config in your folder containing the view as you have in /views

  3. Adrian on July 19th, 2013

    Have discovered the key to this is in the detail, like the web.config in your Views Directory. I would highlight that in this document ideally.

  4. Martin on August 15th, 2013

    Hi Tim,

    Trying to modify this into my own setup.
    However, clicking on a node leaves the page blank.

    I’ve set the routeconfig, and it does get added to the routescollection. But in some way, the page turns up completely blank. If I remove the routeconfig, I’ll get a 404 error.

    Would you know anything to point me in the right direction?

    Martin

  5. Tim Geyssens on August 15th, 2013

    @Martin how does the view for the page look is there some markup on there?

  6. Martin Lingstuyl on August 15th, 2013

    Basically i just used your demopage and changed the model and the formfields. Do you think it’s got something to do with that? How would i know if there’s an error anywhere? Nothing shows up in umbracoLog. And firebug says the blank page returns 200 ok.

    Some extra info: when adding breakpoints the routeconfig will get hit, but the controller won’t. I can’t seem to add break points to the cshtml page, because of symbols not loaded.

    I did try changing some of the names of the controller in the forms and the config, doesn’t seem to work either.

  7. Martin Lingstuyl on August 15th, 2013

    Hmmm, I just created a new controller, as default as possible, and this seems to work now

    How does the route config ‘controller’ default string connect to the right class? As it now seems to not have recognised my original controller class.

  8. Martin on August 16th, 2013

    Hi Tim,

    Is there a simple way to use the umbraco tabs and for example the richtext editor in all this?

    Martin

  9. Martin Lingstuyl on August 16th, 2013

    AH, i discovered that the classname of a controller should actually end with the word ‘controller’. https://www.simple-talk.com/dotnet/asp.net/asp.net-mvc-controllers-and-conventions/
    Thats why mine didnt work. I remember thinking this possibility throughout debugging and dismissing it for seeming too ridiculous a notion :-) maybe i need to update my mvc knowledge. Still think it’s ridiculous though…

    Anyway, its working now. Thanks for the great example!

    Martin

  10. Tim Geyssens on August 17th, 2013

    @Martin no example of tabs but just take a look at how the webforms stuff is done you should be able to reuse the markup/js

  11. Martin on August 23rd, 2013

    Hi Tim / Any others who are interested.

    I looked into it and created some MVC Html Helpers to create tabs with richtext editors. Used it for a project of mine.

    Below a link to a blog and download. It took me a while to grind through all the scripts and html requirements, so it’s only logical anyone else shouldn’t have to go through that :)
    http://interactivewebdesign.nl/sharing/blog/2013/august/umbraco-custom-section-mvc-tabview-and-richtext/

  12. Tim Geyssens on August 26th, 2013

    @Martin awesome :)

  13. Pavel Gurecki on November 4th, 2013

    Great post!

    Any ideas how to use umbraco property editors in custom section mvc view?

  14. Tim Geyssens on November 4th, 2013

    @Pavel, prop editors are web controls so you can’t :) maybe in v7 this will be possible

  15. Karthik on January 22nd, 2014

    Really a great post, helped me to create my own node to list the unapproved members in Members section. Thanks a lot! :)

  16. Moran monovich (@moranmono) on February 22nd, 2014

    Thanks a lot great post and project

  17. marchande d'amour bi on April 27th, 2014

    Оn peut te dire que ce n’est pas incohérent !

  18. Dom Hemingway Télécharger on April 28th, 2014

    You’re so cool! I don’t suppose I’ve truly read through a single thing like this before.
    So good to find someone with a few unique thoughts on this topic.
    Seriously.. thanks for starting this up. This website is something that is required on the web, someone with some originality!

    Visit my website: Dom Hemingway Télécharger

  19. NZ on July 21st, 2014

    Hi Tim,

    Thanks for this post. This is exactly what I am looking for!

    However I am trying it with Umbraco 7 and the javascript doesn’t seem to be rendered.

    When clicking on a person’s node I am getting:

    Error evaluating js callback from legacy tree node: ReferenceError: openExamplePage is not defined

    Is there any reason for the RenderJS not to be called?

    Thanks!

  20. NZ on July 22nd, 2014

    Hi Tim,

    Javascript issue sort, please ignore the above.

    Do you have by any chance an example for unmbraco 7 as well? So when expanding the example tree, an ‘Add’ button will show in the pane the opens? And the view styled as the Umbraco 7 style?

    Thanks!

  21. propolis grosir on September 1st, 2014

    Organic apple cider vindgar treatment іs rich in potassium,
    enzymes аnd calcium. Ҭhey miցht ɑlso bе coknsumed as syrups, tablets ߋr powders.
    Thhe blue crab, аs a attainable instance, provides livelihood tо yoսr coastal communities frοm
    the Gulf of Mexico, aѕ siցnificant industrial capture fisheries supply ѡork opportunities tօgether witҺ economic guidance.

Leave a Reply