diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index 5e840610a5..373cd65436 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -172,6 +172,7 @@ False ..\..\lib\WebPages\System.Web.Helpers.dll + False ..\..\lib\WebPages\System.Web.Razor.dll @@ -1663,6 +1664,7 @@ + Web.Template.config Designer diff --git a/src/Umbraco.Web.UI/Web.config b/src/Umbraco.Web.UI/Web.config index a860662208..3dd62de184 100644 --- a/src/Umbraco.Web.UI/Web.config +++ b/src/Umbraco.Web.UI/Web.config @@ -36,7 +36,7 @@ - + diff --git a/src/Umbraco.Web/AreaRegistrationExtensions.cs b/src/Umbraco.Web/AreaRegistrationExtensions.cs index ba9697246f..539b8efc16 100644 --- a/src/Umbraco.Web/AreaRegistrationExtensions.cs +++ b/src/Umbraco.Web/AreaRegistrationExtensions.cs @@ -14,29 +14,24 @@ namespace Umbraco.Web /// Creates a custom individual route for the specified controller plugin. Individual routes /// are required by controller plugins to map to a unique URL based on ID. /// - /// An PluginAttribute /// /// /// An existing route collection - /// the data token name for the controller plugin /// /// The suffix name that the controller name must end in before the "Controller" string for example: - /// ContentTreeController has a controllerSuffixName of "Tree" - /// - /// - /// The base name of the URL to create for example: Umbraco/[PackageName]/Trees/ContentTree/1 has a baseUrlPath of "Trees" + /// ContentTreeController has a controllerSuffixName of "Tree", this is used for route constraints. /// /// /// /// - /// /// The DataToken value to set for the 'umbraco' key, this defaults to 'backoffice' - internal static void RouteControllerPlugin(this AreaRegistration area, Guid controllerId, string controllerName, Type controllerType, RouteCollection routes, - string routeIdParameterName, string controllerSuffixName, string baseUrlPath, string defaultAction, object defaultId, + /// + /// + internal static void RouteControllerPlugin(this AreaRegistration area, string controllerName, Type controllerType, RouteCollection routes, + string controllerSuffixName, string defaultAction, object defaultId, string umbracoTokenValue = "backoffice") { Mandate.ParameterNotNullOrEmpty(controllerName, "controllerName"); - Mandate.ParameterNotNullOrEmpty(routeIdParameterName, "routeIdParameterName"); Mandate.ParameterNotNullOrEmpty(controllerSuffixName, "controllerSuffixName"); Mandate.ParameterNotNullOrEmpty(defaultAction, "defaultAction"); Mandate.ParameterNotNull(controllerType, "controllerType"); @@ -45,30 +40,24 @@ namespace Umbraco.Web var umbracoArea = GlobalSettings.UmbracoMvcArea; - //routes are explicitly name with controller names. - var url = baseUrlPath.IsNullOrWhiteSpace() - ? umbracoArea + "/" + area.AreaName + "/" + controllerName + "/{action}/{id}" - : umbracoArea + "/" + area.AreaName + "/" + baseUrlPath + "/" + controllerName + "/{action}/{id}"; + //routes are explicitly name with controller names and IDs + var url = umbracoArea + "/" + area.AreaName + "/" + controllerName + "/{action}/{id}"; //create a new route with custom name, specified url, and the namespace of the controller plugin var controllerPluginRoute = routes.MapRoute( //name - string.Format("umbraco-{0}-{1}", controllerName, controllerId), + string.Format("umbraco-{0}", controllerType.FullName), //url format url, //set the namespace of the controller to match new[] { controllerType.Namespace }); - - //TODO: FIx this!! - //By setting the default this will always route even without specifying the surfaceId syntax in the URL, - // we need to unit test this and ensure it is correct - + //set defaults controllerPluginRoute.Defaults = new RouteValueDictionary( new Dictionary { { "controller", controllerName }, - { routeIdParameterName, controllerId.ToString("N") }, + { "controllerType", controllerType.FullName }, { "action", defaultAction }, { "id", defaultId } }); @@ -78,7 +67,7 @@ namespace Umbraco.Web new Dictionary { { "controller", @"(\w+)" + controllerSuffixName }, - { routeIdParameterName, Regex.Escape(controllerId.ToString("N")) } + { "controllerType", controllerType.FullName } }); diff --git a/src/Umbraco.Web/Mvc/PluginController.cs b/src/Umbraco.Web/Mvc/PluginController.cs new file mode 100644 index 0000000000..355aca89e6 --- /dev/null +++ b/src/Umbraco.Web/Mvc/PluginController.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Concurrent; +using System.Web.Mvc; +using Umbraco.Core; + +namespace Umbraco.Web.Mvc +{ + /// + /// A base class for all plugin controllers to inherit from + /// + public abstract class PluginController : Controller, IRequiresUmbracoContext + { + /// + /// stores the metadata about plugin controllers + /// + private static readonly ConcurrentDictionary Metadata = new ConcurrentDictionary(); + + /// + /// Default constructor + /// + /// + protected PluginController(UmbracoContext umbracoContext) + { + UmbracoContext = umbracoContext; + InstanceId = Guid.NewGuid(); + } + + /// + /// Useful for debugging + /// + internal Guid InstanceId { get; private set; } + + public UmbracoContext UmbracoContext { get; set; } + + /// + /// Returns the metadata for this instance + /// + internal PluginControllerMetadata GetMetadata() + { + PluginControllerMetadata meta; + if (Metadata.TryGetValue(this.GetType(), out meta)) + { + return meta; + } + + var attribute = this.GetType().GetCustomAttribute(false); + + meta = new PluginControllerMetadata() + { + AreaName = attribute == null ? null : attribute.AreaName, + ControllerName = ControllerExtensions.GetControllerName(this.GetType()), + ControllerNamespace = this.GetType().Namespace, + ControllerType = this.GetType() + }; + + Metadata.TryAdd(this.GetType(), meta); + + return meta; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Mvc/PluginControllerArea.cs b/src/Umbraco.Web/Mvc/PluginControllerArea.cs new file mode 100644 index 0000000000..fe981b815a --- /dev/null +++ b/src/Umbraco.Web/Mvc/PluginControllerArea.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web.Mvc; +using System.Web.Routing; +using Umbraco.Core; +using Umbraco.Core.Configuration; + +namespace Umbraco.Web.Mvc +{ + /// + /// A custom area for controllers that are plugins + /// + internal class PluginControllerArea : AreaRegistration + { + private readonly IEnumerable _surfaceControllers; + private readonly string _areaName; + + /// + /// The constructor accepts all types of plugin controllers and will verify that ALL of them have the same areaName assigned to them + /// based on their PluginControllerAttribute. If they are not the same an exception will be thrown. + /// + /// + public PluginControllerArea(IEnumerable pluginControllers) + { + //TODO: When we have other future plugin controllers we need to combine them all into one list here to do our validation. + var controllers = pluginControllers.ToArray(); + + if (controllers.Any(x => x.GetMetadata().AreaName.IsNullOrWhiteSpace())) + { + throw new InvalidOperationException("Cannot create a PluginControllerArea unless all plugin controllers assigned have a PluginControllerAttribute assigned"); + } + _areaName = controllers.First().GetMetadata().AreaName; + foreach(var c in controllers) + { + if (c.GetMetadata().AreaName != _areaName) + { + throw new InvalidOperationException("Cannot create a PluginControllerArea unless all plugin controllers assigned have the same AreaName. The first AreaName found was " + _areaName + " however, the controller of type " + c.GetType().FullName + " has an AreaName of " + c.GetMetadata().AreaName); + } + } + + //get the surface controllers + _surfaceControllers = controllers.OfType(); + } + + public override void RegisterArea(AreaRegistrationContext context) + { + MapRouteSurfaceControllers(context.Routes, _surfaceControllers); + } + + public override string AreaName + { + get { return _areaName; } + } + + /// + /// Registers all surface controller routes + /// + /// + /// + private void MapRouteSurfaceControllers(RouteCollection routes, IEnumerable surfaceControllers) + { + foreach (var s in surfaceControllers) + { + var meta = s.GetMetadata(); + this.RouteControllerPlugin(meta.ControllerName, meta.ControllerType, routes, "Surface", "Index", UrlParameter.Optional, "surface"); + } + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Mvc/PluginControllerAttribute.cs b/src/Umbraco.Web/Mvc/PluginControllerAttribute.cs new file mode 100644 index 0000000000..064f39826d --- /dev/null +++ b/src/Umbraco.Web/Mvc/PluginControllerAttribute.cs @@ -0,0 +1,26 @@ +using System; +using System.Linq; + +namespace Umbraco.Web.Mvc +{ + /// + /// An attribute applied to a plugin controller that requires that it is routed to its own area + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] + public class PluginControllerAttribute : Attribute + { + public string AreaName { get; private set; } + + public PluginControllerAttribute(string areaName) + { + //validate this, only letters and digits allowed. + if (areaName.Any(c => !Char.IsLetterOrDigit(c))) + { + throw new FormatException("The areaName specified " + areaName + " can only contains letters and digits"); + } + + AreaName = areaName; + } + + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Mvc/SurfaceControllerMetadata.cs b/src/Umbraco.Web/Mvc/PluginControllerMetadata.cs similarity index 73% rename from src/Umbraco.Web/Mvc/SurfaceControllerMetadata.cs rename to src/Umbraco.Web/Mvc/PluginControllerMetadata.cs index ca95965de8..257fc25fe5 100644 --- a/src/Umbraco.Web/Mvc/SurfaceControllerMetadata.cs +++ b/src/Umbraco.Web/Mvc/PluginControllerMetadata.cs @@ -5,11 +5,11 @@ namespace Umbraco.Web.Mvc /// /// Represents some metadata about the surface controller /// - internal class SurfaceControllerMetadata + internal class PluginControllerMetadata { internal Type ControllerType { get; set; } internal string ControllerName { get; set; } internal string ControllerNamespace { get; set; } - internal Guid? ControllerId { get; set; } + internal string AreaName { get; set; } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Mvc/SurfaceController.cs b/src/Umbraco.Web/Mvc/SurfaceController.cs index a4ceeed4f6..6d13f3b130 100644 --- a/src/Umbraco.Web/Mvc/SurfaceController.cs +++ b/src/Umbraco.Web/Mvc/SurfaceController.cs @@ -6,12 +6,13 @@ using Umbraco.Core; namespace Umbraco.Web.Mvc { - [SurfaceController("DD307F95-6D90-4593-8C97-093AC7C12573")] + [PluginController("MyTestSurfaceController")] public class TestSurfaceController : SurfaceController { public ActionResult Index() { - return Content("hello"); + return View(); + //return Content("hello"); } } @@ -19,37 +20,24 @@ namespace Umbraco.Web.Mvc /// The base controller that all Presentation Add-in controllers should inherit from /// [MergeModelStateToChildAction] - public abstract class SurfaceController : Controller, IRequiresUmbracoContext - { - ///// - ///// stores the metadata about surface controllers - ///// - //private static ConcurrentDictionary _metadata = new ConcurrentDictionary(); - - public UmbracoContext UmbracoContext { get; set; } - - /// - /// Useful for debugging - /// - internal Guid InstanceId { get; private set; } + public abstract class SurfaceController : PluginController + { /// /// Default constructor /// /// protected SurfaceController(UmbracoContext umbracoContext) - { - UmbracoContext = umbracoContext; - InstanceId = Guid.NewGuid(); + : base(umbracoContext) + { } /// /// Empty constructor, uses Singleton to resolve the UmbracoContext /// protected SurfaceController() - { - UmbracoContext = UmbracoContext.Current; - InstanceId = Guid.NewGuid(); + : base(UmbracoContext.Current) + { } /// @@ -105,20 +93,6 @@ namespace Umbraco.Web.Mvc } } - /// - /// Returns the metadata for this instance - /// - internal SurfaceControllerMetadata GetMetadata() - { - var controllerId = this.GetType().GetCustomAttribute(false); - - return new SurfaceControllerMetadata() - { - ControllerId = controllerId == null ? null : (Guid?) controllerId.Id, - ControllerName = ControllerExtensions.GetControllerName(this.GetType()), - ControllerNamespace = this.GetType().Namespace, - ControllerType = this.GetType() - }; - } + } } \ No newline at end of file diff --git a/src/Umbraco.Web/Mvc/SurfaceControllerArea.cs b/src/Umbraco.Web/Mvc/SurfaceControllerArea.cs deleted file mode 100644 index 7ac8b29ee8..0000000000 --- a/src/Umbraco.Web/Mvc/SurfaceControllerArea.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Web.Mvc; -using System.Web.Routing; -using Umbraco.Core; -using Umbraco.Core.Configuration; - -namespace Umbraco.Web.Mvc -{ - /// - /// A custom area for surface controller routes - /// - internal class SurfaceControllerArea : AreaRegistration - { - private readonly IEnumerable _surfaceControllers; - - public SurfaceControllerArea(IEnumerable surfaceControllers) - { - _surfaceControllers = surfaceControllers; - } - - public override void RegisterArea(AreaRegistrationContext context) - { - MapRouteSurfaceControllers(context.Routes, _surfaceControllers); - } - - public override string AreaName - { - get { return "Surface"; } - } - - /// - /// Registers all surface controller routes - /// - /// - /// - private void MapRouteSurfaceControllers(RouteCollection routes, IEnumerable surfaceControllers) - { - var areaName = GlobalSettings.UmbracoMvcArea; - - //local surface controllers do not contain the attribute - var localSurfaceControlleres = surfaceControllers.Where(x => x.GetType().GetCustomAttribute(false) == null); - foreach (var s in localSurfaceControlleres) - { - var meta = s.GetMetadata(); - var route = routes.MapRoute( - string.Format("umbraco-{0}-{1}", "surface", meta.ControllerName), - areaName + "/Surface/" + meta.ControllerName + "/{action}/{id}",//url to match - new { controller = meta.ControllerName, action = "Index", id = UrlParameter.Optional }, - new[] { meta.ControllerNamespace }); //only match this namespace - route.DataTokens.Add("area", areaName); //only match this area - route.DataTokens.Add("umbraco", "surface"); //ensure the umbraco token is set - } - - var pluginSurfaceControllers = surfaceControllers.Where(x => x.GetType().GetCustomAttribute(false) != null); - foreach (var s in pluginSurfaceControllers) - { - var meta = s.GetMetadata(); - this.RouteControllerPlugin(meta.ControllerId.Value, meta.ControllerName, meta.ControllerType, routes, "surfaceId", "Surface", "", "Index", UrlParameter.Optional, "surface"); - } - } - } -} \ No newline at end of file diff --git a/src/Umbraco.Web/Mvc/SurfaceControllerAttribute.cs b/src/Umbraco.Web/Mvc/SurfaceControllerAttribute.cs deleted file mode 100644 index ac08ca9886..0000000000 --- a/src/Umbraco.Web/Mvc/SurfaceControllerAttribute.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System; - -namespace Umbraco.Web.Mvc -{ - /// - /// An attribute applied to surface controllers that are not locally declared (i.e. they are shipped as part of a package) - /// - [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] - public class SurfaceControllerAttribute : Attribute - { - public Guid Id { get; private set; } - - public SurfaceControllerAttribute(string id) - { - Guid gid; - if (Guid.TryParse(id, out gid)) - { - Id = gid; - } - else - { - throw new InvalidCastException("Cannot convert the value " + id + " to a Guid"); - } - } - } -} \ No newline at end of file diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index cce5ce9568..caea0cb4ea 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -273,6 +273,7 @@ + @@ -281,10 +282,10 @@ Strings.resx - - + + - + diff --git a/src/Umbraco.Web/WebBootManager.cs b/src/Umbraco.Web/WebBootManager.cs index 483a3b090f..e44528525c 100644 --- a/src/Umbraco.Web/WebBootManager.cs +++ b/src/Umbraco.Web/WebBootManager.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Web.Mvc; using System.Web.Routing; using Umbraco.Core; @@ -124,12 +125,42 @@ namespace Umbraco.Web ); defaultRoute.RouteHandler = new RenderRouteHandler(ControllerBuilder.Current.GetControllerFactory()); - //now we need to find the surface controllers and route them too - var surfaceControllers = SurfaceControllerResolver.Current.SurfaceControllers; - //create a custom area for them - var surfaceControllerArea = new SurfaceControllerArea(surfaceControllers); - //register it - RouteTable.Routes.RegisterArea(surfaceControllerArea); + var umbracoPath = GlobalSettings.UmbracoMvcArea; + + //we need to find the surface controllers and route them + var surfaceControllers = SurfaceControllerResolver.Current.SurfaceControllers.ToArray(); + + //local surface controllers do not contain the attribute + var localSurfaceControlleres = surfaceControllers.Where(x => x.GetType().GetCustomAttribute(false) == null); + foreach (var s in localSurfaceControlleres) + { + var meta = s.GetMetadata(); + var route = RouteTable.Routes.MapRoute( + string.Format("umbraco-{0}-{1}", "surface", meta.ControllerName), + umbracoPath + "/Surface/" + meta.ControllerName + "/{action}/{id}",//url to match + new { controller = meta.ControllerName, action = "Index", id = UrlParameter.Optional }, + new[] { meta.ControllerNamespace }); //only match this namespace + route.DataTokens.Add("area", umbracoPath); //only match this area + route.DataTokens.Add("umbraco", "surface"); //ensure the umbraco token is set + } + + //need to get the plugin controllers that are unique to each area (group by) + //TODO: One day when we have more plugin controllers, we will need to do a group by on ALL of them to pass into the ctor of PluginControllerArea + var groupedAreas = surfaceControllers.GroupBy(controller => controller.GetMetadata().AreaName); + //loop through each area defined amongst the controllers + foreach(var g in groupedAreas) + { + //create an area for the controllers (this will throw an exception if all controllers are not in the same area) + var pluginControllerArea = new PluginControllerArea(g); + //register it + RouteTable.Routes.RegisterArea(pluginControllerArea); + } + + RouteTable.Routes.MapRoute( + "Account", + "account/{action}/{id}", + new { controller = "Account", action = "Index", id = UrlParameter.Optional } + ); }