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
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
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 (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
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)
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
@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
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.
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
@Martin how does the view for the page look is there some markup on there?
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.
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.
Hi Tim,
Is there a simple way to use the umbraco tabs and for example the richtext editor in all this?
Martin
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/
maybe i need to update my mvc knowledge. Still think it’s ridiculous though…
Thats why mine didnt work. I remember thinking this possibility throughout debugging and dismissing it for seeming too ridiculous a notion
Anyway, its working now. Thanks for the great example!
Martin
@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
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/
@Martin awesome
Great post!
Any ideas how to use umbraco property editors in custom section mvc view?
@Pavel, prop editors are web controls so you can’t
maybe in v7 this will be possible
Really a great post, helped me to create my own node to list the unapproved members in Members section. Thanks a lot!
Thanks a lot great post and project
Оn peut te dire que ce n’est pas incohérent !
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
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!
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!
Hi Tim ,
I also tried with Umbraco 7 but the javascript doesn’t seem to be rendered in tree item.
Same issue with @NZ
Could you please take a look and give us some advice.
Thanks
Hi
Do you have an example to create a new html page in Umbraco Backoffice with a button to perform an AJAX Call…
I’ll appreciate any help
Meep City: A role-taking part in game impressed by the now defunct recreation Club Penguin (now Membership Penguin Island ) and
ToonTown (now ToonTown Rewritten), and is often considered a carbon copy
of those two video games attributable to varied similarities.
A number of the updates break a few of the Lua scripts in games,
thus breaking functionality of a number of the video games.