I'm looking to create a common ASP.MVC (c# and razor) base project containing common controllers, _Layout.cshtml, css and js for many of our webapps to extend from.
I thought that using MvcContrib and creating Portable Areas is my best bet
So the folder setup is roughly like this
BaseProj
Content
js
plugins
misc
images
foo
css
Controllers
Views
MainProj
Content
Controllers
Views
I am registering the BaseProj area by extending PortableAreaRegistration class (as per MvcContrib docs) so this url works...
htttp://localhost/MainProj/BaseProj/
Looking in the MvcContrib code for PortableAreaRegistration it also registers 3 static routes too (images, styles, scripts) under Content.
Therefore this url works fine too...
htttp://localhost/MainProj/BaseProj/images/bar.jpg
also subfolders work fine only if i use a dot instead of a slash...
htttp://localhost/MainProj/BaseProj/images/foo.bar.jpg
However its really useful to have subfolders under images, css, js etc for organisation purposes. These subfolders will now 404 though
ie this will not work....
htttp://localhost/MainProj/BaseProj/images/foo/bar.jpg
So the question is how do i map subfolders under images (without the dot notation)?
ie so this works....
htttp://localhost/MainProj/BaseProj/images/foo/bar.jpg
Thanks
EDIT:
Well a bit of thinking and I found a solution. I wonder if this kind of thing might be included in MvcContrib.Portable Areas by default.
First create a catch all route in your BaseProjRegistration.cs to catch all subfolders of Content
public override void RegisterArea(AreaRegistrationContext context, IApplicationBus bus)
{
...
//static controller with catch all for all subfolders of content
context.MapRoute(
"BaseProjContent",
"BaseProj/Content/{*resourceName}",
new { controller = "Content", action = "LoadContent", resourcePath = "Content" }
);
...
}
Then Create a ContentController class in your BaseProj which will extend MvcContrib.PortableAreas.EmbeddedResourceController.
This will convert slashes to dots to load the BaseProj content from the DLL
ie /BaseProj/Content/images/foo/bar.jpg => /BaseProj/Content/images.foo.bar.jpg
public class ContentController : MvcContrib.PortableAreas.EmbeddedResourceController
{
public ActionResult LoadContent(string resourceName, string resourcePath)
{
string actualResourceName = resourceName.Replace("/", ".");
return base.Index(actualResourceName, resourcePath);
}
}
This worked for me, but any other recommnedations of how to setup a common base project for lots of webapps to extend are welcome
Related
I have read a nice article about how I can use feature folder structure in my ASP.NET Core MVC application. My plan is to use then a feature folder structure to organize my web application in a better way.
First of all lets see my folder structure:
...
wwwroot
Claims
Controllers
Services
Views
Shared
Map
...
I have followed the article and I implemented the IViewLocationExpander like the following:
public class MyViewLocationExpander : IViewLocationExpander
{
public void PopulateValues(ViewLocationExpanderContext context)
{
context.Values["customviewlocation"] = nameof(MyViewLocationExpander);
}
public IEnumerable<string> ExpandViewLocations(ViewLocationExpanderContext context, IEnumerable<string> viewLocations)
{
var viewLocationFormats = new[]
{
"/Claims/Views/{0}.cshtml",
"/Claims/Views/Shared/{0}.cshtml"
};
return viewLocationFormats;
}
}
I placed my main Claims.cshtml view in the "/Claims/Views/" folder. At the beginning of my Claims.cshtml I have to following line to render my partial view:
#Html.Partial("_NewClaimPopup");
As for the _NewClaimPopup.cshtml, it placed it into the path "/Claims/Views/Shared". But unfortunately I got the following exception when trying to GET the following url: http://localhost:13078/Claims/Claims
InvalidOperationException: The partial view '_NewClaimPopup' was not found. The following locations were searched:
/Views/Claims/_NewClaimPopup.cshtml
/Views/Shared/_NewClaimPopup.cshtml
It seems that the custom paths are successfully added by the implementation of IViewLocationExpander.
Additional infos:
What I also tried is to use "~" sign in paths of the implementation of IViewLocationExpander, so: "~/Claims/Views/{0}.cshtml" and "~/Claims/Views/Shared/{0}.cshtml" but it does not help.
I tried to use absolute path for rendering my partial view, but still nothing
#Html.Partial(""~/Claims/Views/_NewClaimPopup.cshtml");
And of course, I registered my expander in the Startup.cs:
services.Configure<RazorViewEngineOptions>(options => options.ViewLocationExpanders.Add(new MyViewLocationExpander()));
Last but not least I attach a picture about my project structure:
Any other idea? Thanks in advance for any help!
I have made sample application. You can download from below link
Custom View Location
I have a C# MVC Razor site. Typically, Controllers load views from the Views folder. However, I have a special circumstance where I need to render a view outside of the Views folder. How do I do that?
Controller will load /Views/Random/Index.cshtml
Can't load /Random/Index.cshtml
/Random/test.aspx loads with no issues, but can't change cshtml files to aspx files, they need to be built regularly.
I have tried return Redirect("/Random/Index.cshtml") in the Controller, and currently have no controller at all.
The weird thing is it works on my Production environment, but not in localhost. In localhost I get:
The type of page you have requested is not served because it has been explicitly forbidden. The extension '.cshtml' may be incorrect. Please review the URL below and make sure that it is spelled correctly.
Requested URL: /Random/Index.cshtml
You can definitely do this. For doing this you need to create one new custom view engine like
public class MyViewEngine : RazorViewEngine
{
private static string[] AdditionalViewLocations = new[]{
"~/Random/{0}.cshtml"
};
public MyViewEngine()
{
base.PartialViewLocationFormats = base.PartialViewLocationFormats.Union(AdditionalViewLocations).ToArray();
base.ViewLocationFormats = base.ViewLocationFormats.Union(AdditionalViewLocations).ToArray();
base.MasterLocationFormats = base.MasterLocationFormats.Union(AdditionalViewLocations).ToArray();
}
}
Then in you global.asax's Application_Start method register this view engine like this-
ViewEngines.Engines.Add(new MyViewEngine ());
If you want your viewengine to take precedence then insert this at 0th position. like this -
ViewEngines.Engines.Insert(0, new MyViewEngine());
return View("~/AnotherFolder/Index.cshtml")` should work for you.
Do not forget to indicate the Layout in your index view:
#{
Layout="~/Views/Shared/Layout.cshtml";
}
In MVC 4, you can just append .Mobile to any view and mobile devices will automatically get served that view from the same controller. Is there a way to store the .Mobile files in a different folder? I really want to store the desktop files in one "Area" and the mobile in another "Area". Anyone know of something like this?
This can easily be accomplished by creating a custom implementation of RazorViewEngine and adding the custom mappings to the ViewLocationFormats. It is important to remember to add the custom mappings to the beginning of the ViewLocationFormats array as they are more specific than the existing mappings.
namespace MobileViewsInMobileFolder.Utility {
public class MyCustomViewEngine : RazorViewEngine {
public MyCustomViewEngine() {
List<string> existingViewLocationFormats = ViewLocationFormats.ToList();
//Folder Structure: Views\Home\Desktop and Views\Home\Mobile
existingViewLocationFormats.Insert(0, "~/Views/{1}/Desktop/{0}.cshtml");
existingViewLocationFormats.Insert(0, "~/Views/{1}/Mobile/{0}.cshtml");
//Folder Structure: Views\Desktop\Home and Views\Mobile\Home
existingViewLocationFormats.Insert(0, "~/Views/Desktop/{1}/{0}.cshtml");
existingViewLocationFormats.Insert(0, "~/Views/Mobile/{1}/{0}.cshtml");
ViewLocationFormats = existingViewLocationFormats.ToArray();
}
}
}
And then make sure to add the custom view engine in the Application_Start
ViewEngines.Engines.Clear();
ViewEngines.Engines.Add(new MyCustomViewEngine());
I recently came across a way to develop pluggable application modules when using ASP.NET MVC3/4 and I loved the simplicity of the approach. Currently, I have my applications structured as follows:
Therefore, anyone wanting to develop an extension for my application, follows the approach from the above tutorial and creates an extension that stays in the Areas folder. I figure that when new Areas (created as new projects) are added, .pdb files are created and placed in the bin directory. My question is this:
How does one distribute the Areas as pluggable modules?
How do I change the following code so that when someone drops a new Area into the bin folder, the application automatically picks it up and creates a link? And what should the plugin author do to enable this?
In my _Layout.cshtml (global shared layout), I do the following to construct the links:
<ul>
<li>#Html.ActionLink("Area1", "Index", "Area1", new { Area = "Area1" }, null)</li>
<li>#Html.ActionLink("Area2", "Index", "Area2", new { Area = "Area2" }, null)</li>
<li>#Html.ActionLink("Area3", "Index", "Area3", new { Area = "Area3" }, null)</li>
</ul>
For simplicity, assume that the area names are unique. Any suggestions on how to do this?
How does one distribute the Areas as pluggable modules?
Don't create the Areas in the hosting web app, but create separate projects, that compile to separate dll's. Copy the dll's to any web app where you want to use it. Remember to set all static files as "EmbeddedResource".
How do I change the following code so that when someone drops a new Area into the bin folder, the application automatically picks it up and creates a link? And what should the plugin author do to enable this?
You can use MvcContrib PortableAreaRegistration's "Bus" to send messages, commands from the portable area to anyone on the 'bus'. That can be the hosting web app, or in theory independent Area's can send message to each other.
Created two crude, but functional demo of this, code on github:
MVC3 version:
https://github.com/AkosLukacs/PluggablePortableAreas
MVC4 RC version:
https://github.com/AkosLukacs/PluggablePortableAreasMVC4
First, you define a message that can carry the information you need. Just a POCO that has some properties (PluggablePortableAreas.Common\RegisterAreaMessage.cs):
public class RegisterAreaMessage : IEventMessage
{
public string LinkText { get; private set; }
public string AreaName { get; private set; }
public string ControllerName { get; private set; }
public string ActionName { get; private set; }
//...
}
Create a handler for that message type (PluggablePortableAreas.Common\RegisterAreaEventHandler.cs):
public class RegisterAreaEventHandler : MessageHandler<RegisterAreaMessage>{}
In this case, the MessageHandler just adds the received messages to a static ConcurrentBag<RegisterAreaMessage>. You can use DI, if you want to, but wanted to keep it simple.
You can send the message from the portable area like this (Areas\DemoArea1\Areas\Demo1\Demo1AreaRegistration.cs):
//the portable area sends a message to the 'bus'
bus.Send(new RegisterAreaMessage("Link to the Demo area", AreaName, DefaultController, DefaultAction));
The dynamically added links are displayed by iterating over the collection of messages(PluggablePortableAreas.Web\Views\Shared_Layout.cshtml):
#foreach(var sor in PluggablePortableAreas.Common.RegisterAreaEventHandler.RegisteredAreas) {
<li>#Html.ActionLink(sor.LinkText, sor.ActionName, sor.ControllerName, new{Area=sor.AreaName}, null)</li>
}
One more thing to take care of: Use "fully qualified" Area names. If you don't specify the area name explicitly, MVC assumes it's the current area. No problems without areas, but the second will point to "/path_to_your_app/CurrentArea/Home" instead of "/path_to_your_app/Home".
<li>#Html.ActionLink("Home", "Index", "Home", new { Area = "" }, null)</li>
<li>#Html.ActionLink("I don't work from the portable area!", "Index", "Home")</li>
Even one more thing to note!
The development server in VS feels a bit "erratic", sometimes the portable area doesn't load. Works reliably in full IIS tho...
What you're doing looks very much alike the MVCContrib's Portable Areas.
They use the message/application bus pattern to dynamically add new widgets on web elements.
MessageHandlers are discovered via reflection and each message is passed to all of them. So in your case the plugin author should just implement a handler for a standard message(say register global menu link).
Resources of the Portable Areas are embedded so just a single dll could be dropped in the bin folder. In order to automatically pick it up and use it in your app you will have to watch the bin folder say via FileSystemWatcher and restart your app(there is no other way to load the new .dll into the AppDomain in asp.net application I think).
You may load .dlls from other folders as well by using BuildManager functionality in ASP.NET 4. More useful info on that here.
This looks similar to how Orchard CMS does modules.
Take a look at their Gallery... The modules are distributed as Nuget packages, containing the whole module project.
Hi you can look for pretty nice plug in architecture in nopcommerce project.
take a look at this tutorial I think it'll help you a lot.
I have only tried this in single project areas. So if anyone tries this in a multi-project areas solution please let us know.
Area support was added to MVC2. However the views for your controllers have to be in your main Views folder. The solution I present here will allow you to keep your area specific views in each area. If your project is structured like below, with Blog being an area.
+ Areas <-- folder
+ Blog <-- folder
+ Views <-- folder
+ Shared <-- folder
Index.aspx
Create.aspx
Edit.aspx
+ Content
+ Controllers
...
ViewEngine.cs
Add this code to the Application_Start method in Global.asax.cs. It will clear your current view engines and use our new ViewEngine instead.
// Area Aware View Engine
ViewEngines.Engines.Clear();
ViewEngines.Engines.Add(new AreaViewEngine());
Then create a file named ViewEngine.cs and add the code below.
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Web.Mvc;
namespace MyNamespace
{
public class AreaViewEngine : WebFormViewEngine
{
public AreaViewEngine()
{
// {0} = View name
// {1} = Controller name
// Master Page locations
MasterLocationFormats = new[] { "~/Views/{1}/{0}.master"
, "~/Views/Shared/{0}.master"
};
// View locations
ViewLocationFormats = new[] { "~/Views/{1}/{0}.aspx"
, "~/Views/{1}/{0}.ascx"
, "~/Views/Shared/{0}.aspx"
, "~/Views/Shared/{0}.ascx"
, "~/Areas/{1}/Views/{0}.aspx"
, "~/Areas/{1}/Views/{0}.ascx"
, "~/Areas/{1}/Views/Shared/{0}.aspx"
, "~/Areas/{1}/Views/Shared/{0}.ascx"
};
// Partial view locations
PartialViewLocationFormats = ViewLocationFormats;
}
protected override IView CreatePartialView(ControllerContext controllerContext, string partialPath)
{
return new WebFormView(partialPath, null);
}
protected override IView CreateView(ControllerContext controllerContext, string viewPath, string masterPath)
{
return new WebFormView(viewPath, masterPath);
}
} // End Class AreaViewEngine
} // End Namespace
This will find and use the views you have created in your areas.
This is one possible solution that allows me to keep views in the specified area. Does anyone else have a different, better, enhanced solution?
Thanks
I'm sorry to be the one to tell you this, but you must be missing something. I currently have your scenario working out of the box with ASP.NET MVC 2 RC.
I assume you have all the register routes and have the correct web.config files inside your area's view folder?
Maybe have a look at this walk through, especially the part about creating the areas.
HTHs,
Charles
EDIT:
Ok, so you're not happy about putting in the extra new { area = "blog' }, null - fair enough, I'll admit its niggly... but what else are you going to do?
What happens when you have two controllers with the same name? One in your root project and one in an area or two controllers with the same name in two different areas? How is it going to find the correct view?
Also, I do see a problem with your ViewLocationFormats. All of the area view locations have no reference to their area... e.g. ~/Areas/{1}/Views/{0}.ascx - how does it know what area?
If you are suggesting that all the different area's views and all thrown into the Areas folder under their controller name and then found under Views and Views/Shared - I would highly recommend against that... It'll become a mess very quickly.
So where does that leave you? It really leaves you needing to specify the area when creating the route. It really boils down to the fact that although it's niggly having to specify the area, there really is no other way.
This solution works well in Mvc2. It is not necessary in Mvc3.