Dynamically extending features of an application? - c#

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.

Related

How to specify the view name in an MVC action?

I'm learning Asp MVC.
I've been doing WPF MVVM programs for two years already, but i also need to learn ASP which is a common language used in web development in my country as far as i know. And i have also knowledge in c# so i think adjusting will not be very hard, but i'm already facing a lot of problems in making my website work. I tried reading about ASP and MVC but i learn by doing things and from my mistake than reading it. So i decided to give it a try.
I created an EMPTY MVC project using Visual Studio Community Edition 2017
I already created the Layout Page and the First Controller and the First View and its totally working fine.
This is the screenshot
Then i create the second controller. Then the problem comes in.
I created a new controller named NewPostController and ADD View for it like this
But it create another folder with the name of the View and inside it is the view it created
I don't want it to organize that way.
So i dragged the NewPost.cshtml into the admin folder. Run the application then i received an error saying
The resource cannot be found.
Requested URL: /Admin/NewPost
I did a search for a solution but i can't solve the problem
I tried specifying the view name
public ActionResult NewPost()
{
return View("~/Admin/NewPost");
}
Most of the solution i read is specify the View Name. But i can't make it work. What are the things that i missed? Or not understand? Thank you.
MVC have sort of a naming convention where if your controller is named FooController then your views should be keep in a folder name Foo.
Inside this controller you will have your
public ActionResult <name of view>
name exactly the same as the view for easy referencing.
So when you have a view under the Foo Folder and the name of that cshtml file is Hello
then inside the FooController, you have a
public ActionResult Hello(//parameter here){
//body here
}
Hope you understand my explanation.
Also to answer your question. I'm assuming you want the NewPost.cshtml as part of the admin folder. Just add
public ActionResult NewPost()
to your admin controller and then you can use
localhost/admin/NewPost()
If i miss anything or any error, please comment hehe answered this in a bit of a rush
Just move your NewPost action to your AdminController as such:
public class AdminController : Controller
{
public ActionResult Dashboard()
{
return View();
}
// Here you go
public ActionResult NewPost()
{
return View();
}
}
This is default MVC structure if you want both Dashboard and NewPost views to be in the Admin folder

Specify alternate VirtualDirectory when using UrlHelper class

I have the following web application architecture using ASP.Net MVC 5.2.2 with virtual directories for each website.
localhost/auth
localhost/web1
localhost/web2
I'm looking to use the standard Url.Action() routing from all three (n) applications.
I have achieved a partly working example by creating an additional project to house my routing for my web applications to reference.
var routeCollection = new RouteCollection();
var requestContext = HttpContext.Current.Request.RequestContext;
SampleApp.Routing.Web1RouteConfig.RegisterRoutes(routeCollection);
var Web1UrlHelper = new UrlHelper(HttpContext.Current.Request.RequestContext, routeCollection);
This allows me to call
#Web1UrlHelper.Action("Test", "Test", { entityId = new Guid() });
and will render me the correct URL format of {guid}/{controller}/{action} as defined by my Web1RouteConfig class in the SampleApp.Routing project.
The issue is the virtual directory of the calling UrlHelper class is retained by the URL builder.
This means (when called from web2) my url is incorrectly formatted as:
localhost/web2/997d0d58-5f09-4431-9b2e-88a247bd334b/test/test
Rather than:
localhost/web1/997d0d58-5f09-4431-9b2e-88a247bd334b/test/test
After digging I've found that I can pass into my RouteCollection class an instance of a VirtualPathProvider. This is where I've hit a dead-end.
Am I going down a rabbit hole? It sounds like I'm trying to hand-crank too much to achieve what sounds like a simple task.
Many thanks.

DevExpress MVCxGridView in Orchard module - Callbacks don't work

We want to use Orchard for a website. We are creating a custom module/widget for that cms and in that module we want to use a GridView from DevExpress to show data. We got most of it working, but we can't get callbacks to work. With that i mean things like navigating through pages, sorting rows and moving columns.
If we look in the console we can see that the javascript and ajax callbacks are never executed, we can't figure out why that is so. I have found some topics on the DevExpress site and this site about using DevExpress with Orchard, but i couldn't find anything usefull (for my case) in those. We also noticed that the methods of our controller are never called, but cannot figure out why not.
I found that sometimes jQuery can cause problems for DevExpress controls, so i tried removing all jQuery scripts, but that didn't make a difference. Someone also suggested to put a callbackpanel around te gridview, but that didn't work either. I have tried many more things (which i mostly forgot already) but nothing worked so far.
I have also asked the same question on the DevExpress website end the Orchard forums but i'm not getting any answers there, so i thought i'd try my luck here.
I have made an example project in case you want to see what i'm trying to do. The file is 40MB because i added the entire cms to itwith example daabase, including our module. The module is called GridViewTest You can find the source here:http://www.obec.nl/download/Orchard-DevExpress.zip.
I have finally found a solution. It turned out to be a pretty simple one (like usual) and i want to share it here, in case other people want to use DevExpress with Orchard:
In your Orchard module, you have to create a Routes.cs file (in the root of the module). There you have to add this:
using System.Collections.Generic;
using System.Web.Mvc;
using System.Web.Routing;
using Orchard.Mvc.Routes;
namespace CentralStationDataView
{
public class Routes : IRouteProvider
{
public void GetRoutes(ICollection<RouteDescriptor> routes)
{
foreach (var routeDescriptor in this.GetRoutes())
{
routes.Add(routeDescriptor);
}
}
public IEnumerable<RouteDescriptor> GetRoutes()
{
return new[]
{
new RouteDescriptor
{
Priority = 5,
Route = new Route(
"AreaName",
new RouteValueDictionary
{
{ "area", "AreaName" },
{ "controller", "ControllerName" },
{ "action", "ActionName" }
},
new RouteValueDictionary(),
new RouteValueDictionary
{
{ "area", "AreaName" }
},
new MvcRouteHandler())
}
};
}
}
}
You can make the AreaName up as you like, it doesn't matter (as far as i know) what you call it. Make sure that you don't add the "Controller" suffix to the ControllerName.
Then, in your GridView settings you have to add this:
settings.CallbackRouteValues = new { area = "AreaName", Controller = "ControllerName", Action = "ViewDataPartial" };
These values have to all be exactly the same as the values in the Routes.cs file. The "area" property was critical for me, i already had the Routes.cs file and everything, but i didn;t add the area property to the CallbackRouteValues.
The second part of the solution is that you have to make a partial view with only and i stress, only, the GridView inside it. So no scripts, no extra html elements, no text, nothing.

Multiple content subfolders in a MvcContrib PortableArea

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

A Solution for Maintaining Views in Single-Project Areas

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.

Categories