Nibble

Archive for the 'MVC' Category


Extending the Umbraco Backend Using MVC 22

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)