Você está na página 1de 61

Justin Jones 21st February 2013 Version 1.

Setting up an MVC4 Multi-Tenant Site


Contents
Introduction ............................................................................................................................................ 2 Prerequisites ........................................................................................................................................... 2 Requirements Overview ......................................................................................................................... 3 Design Structure (High Level).................................................................................................................. 4 Setting Up................................................................................................................................................ 5 Dynamic Layout Pages .......................................................................................................................... 15 Custom Attribute Setting ...................................................................................................................... 23 Custom Routing & Overrides ................................................................................................................ 26 Partial Views and Strongly Typed Models............................................................................................. 30 IFrame Hosting ...................................................................................................................................... 32 Running Specific Client Scripts .............................................................................................................. 33 MVC Areas, Area Views And Partial Views ........................................................................................... 37 MVC Area Custom Routing, Overrides and Client Specific Views ......................................................... 45 Appendix A Using MVC 4 Functionality to Override Area Controllers Only....................................... 57 Appendix B Overriding View Error ..................................................................................................... 60 Resources .............................................................................................................................................. 61

Justin Jones 21st February 2013 Version 1.1

Introduction
In certain circumstances, you may need to distribute an MVC Application to different clients, who use shared functionality with specific customisations. A visual example of such a case is shown below.

Client 1 Specifics

Client 1 UI

Core Functionality

Client 2 UI Client 2 Specifics

Having all core and client code in a single project distributed to all would be ideal. Unfortunately it is not always possible due to commercial and/or security issues. Without proper management, it also risks damaging the integrity of core functionality as client specific modifications are added. An example would be Generic functionality with if(client == client1) statements. One option would be to load specific parts dynamically, but this can introduce complexities you may wish to avoid (for example they are often harder to debug). This demo provides a structure that would have the functionality shown in the diagram, while avoiding some of the pitfalls mentioned above. It is not a complete business solution and is intentionally simplistic for the purpose of the demonstration. It uses only standard MVC functionality.

Prerequisites
To run through this tutorial, you will need Visual Studio 2012, or Visual Web Developer Express 2012. You will also need to have a basic knowledge of ASP.NET MVC.

Justin Jones 21st February 2013 Version 1.1

Requirements Overview
The list below shows general requirements deemed necessary for a multi-tenant site. Requirement 1 The system must allow for a generic version containing core functionality from which clients can have specific customisations. 2 Clients/Users must be able to have specific themes. The images must not be visible to other customers. 3 Clients must be able to set specific properties within their own project. For example, they may have certain flags (e.g. hide log out button) that they wish to set. 4 Clients must be able to change functions of default functionality (e.g. have a next button go to a different page, or have custom validation). 5 The solution must be able to support partial views. 6 The site must work in an iFrame. 7 The solution must enable clients to include specific JavaScript, such as a heartbeat, which can be reached across all pages. 8 The solution should be able to support Areas in MVC. 9 MVC Areas should be able to change/override default functionality (e.g. have a next button go to a different page, custom validation) and provide client specific views (for example, one client may have paid for custom UI controls and not want competitors to have them).

The rest of this document will detail how the proposed structure can meet these requirements.

Justin Jones 21st February 2013 Version 1.1

Design Structure (High Level)


The structure shown in this demo will have a generic project, to which all core functionality will be added. It will then have a client project that hooks into this functionality and makes customisations as required. In theory, many different clients could be added with their own specific customisations.

The demo structure will rely only on standard .NET/MVC functionality. The generic project will have all the core controllers and views. The client project will reference the generic project and map to the generic controllers. An order of preference will be provided in the MVC routings to use custom controllers/views where required. Generic views will be copied to a folder in the client when the project is built and the client project will have view registrations to ensure the views are found. While the approach is simple, it appears to meet most challenges thrown at it, and could easily be built upon.

Justin Jones 21st February 2013 Version 1.1

Setting Up
Requirement The system must allow for a generic version containing core functionality from which clients can have specific customisations. To meet this requirement, we will need to have a Generic project that is referenced by all other projects that need to use it. It will have routes that are registered with client projects in the Global.asax and build views to a temp file, so the views are readily available to any client project. To do this: 1. Create an ASP.NET MVC4 project called Generic.

2. On the next screen, ensure the project type is empty.

Justin Jones 21st February 2013 Version 1.1 3. You will now have a standard MVC4 Project setup as shown below.

4. Set the project type to Class Library (if it not already). To do this, right click on the Generic Project, select Properties and change the Output type to Class Library as shown below.

5. In the Generic project, right click the references and add a reference to System.Runtime.Remoting and click Ok.

Justin Jones 21st February 2013 Version 1.1 6. At the top level of the Generic project, add a class file called EmbeddedResourceViewEngine.cs and add the following code. Note: the tmp references are critical, as this is where the views will be copied to. Note2: This structure will be replaced with something more detailed when we look at areas later.
using using using using using using System; System.Collections.Generic; System.IO; System.Linq; System.Web; System.Web.Mvc;

namespace Generic { public class EmbeddedResourceViewEngine : RazorViewEngine {

/// <summary> /// Set up the view locations (including the temp locations we will build to) /// and take the embedded views and put them in the tmp file. /// NOTE: This file should not need to be updated for new Areas to become part /// of the solution. /// </summary> public EmbeddedResourceViewEngine() {ViewLocationFormats = new[] {
"~/Views/{1}/{0}.aspx", "~/Views/{1}/{0}.ascx", "~/Views/Shared/{0}.aspx", "~/Views/Shared/{0}.ascx", "~/Views/{1}/{0}.cshtml", "~/Views/{1}/{0}.vbhtml", "~/Views/Shared/{0}.cshtml", "~/Views/Shared/{0}.vbhtml", // Code above will search the client before generic. // Improved Temp Locations // The embedded view will be copied to a tmp folder // using a similar structure to the View Folder "~/tmp/Views/{1}/{0}.cshtml", "~/tmp/Views/{1}/{0}.vbhtml", }; PartialViewLocationFormats = new[] { "~/Views/Shared/{0}.cshtml", //Client first not tested in demo "~/tmp/Views/Shared/{0}.cshtml", //Improved method. }; SaveAllViewsToTempLocation(); }

Justin Jones 21st February 2013 Version 1.1


/// <summary> /// Get the embedded views within the project and save the info to the tmp /// location. /// </summary> private static void SaveAllViewsToTempLocation() { IEnumerable<string> resources = typeof(EmbeddedResourceViewEngine).Assembly.GetManifestResourceNames( ).Where(name => name.EndsWith(".cshtml")); foreach (string res in resources) { SaveViewToTempLocation(res); } }

/// <summary> /// Save Resource To The Temp File. /// </summary> /// <param name="res"></param> private static void SaveViewToTempLocation(string res) { // Get the file path to manipulate and the fileName for re-addition later. string[] resArray = res.Split('.'); // rebuild split to get the paths. string filePath = String.Join("/", resArray, 0, resArray.Count() - 2) + "/"; string fileName = String.Join(".", resArray, resArray.Count() - 2, 2); // replace name of project, with temp file to save to. string rootPath = filePath.Replace("Generic", "~/tmp"); //Set in line with the server folder... rootPath = HttpContext.Current.Server.MapPath(rootPath); if (!Directory.Exists(rootPath)) Directory.CreateDirectory(rootPath); //Save the file to the new location. string saveToLocation = rootPath + fileName; Stream resStream = typeof(EmbeddedResourceViewEngine).Assembly.GetManifestResourceStream(res); System.Runtime.Remoting.MetadataServices.MetaData.SaveStreamToFile(resStream , saveToLocation); } } }

7. Build the Generic project.

Justin Jones 21st February 2013 Version 1.1 8. Create a new Client project (MVC4 Empty I have called mine Client1Basic). 9. Once created, right click the References and add a reference to the shared (Generic) project as shown below.

Justin Jones 21st February 2013 Version 1.1 10. In the client project, update the global.asax file with the code below.
using using using using using using using System; System.Collections.Generic; System.Linq; System.Web; System.Web.Http; System.Web.Mvc; System.Web.Routing;

namespace Client1Basic { // Note: For instructions on enabling IIS6 or IIS7 classic mode, // visit http://go.microsoft.com/?LinkId=9394801 public class MvcApplication : System.Web.HttpApplication { public static void RegisterGlobalFilters(GlobalFilterCollection filters) { filters.Add(new HandleErrorAttribute()); } /// <summary> /// Standard Route registration. /// </summary> /// <param name="routes"></param> public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); routes.MapRoute( "Default", // Route name "{controller}/{action}/{id}", // URL with parameters new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults ); } /// <summary> /// standard application start, with additional code to call generic. /// </summary> protected void Application_Start() { AreaRegistration.RegisterAllAreas(); RegisterGlobalFilters(GlobalFilters.Filters); RegisterRoutes(RouteTable.Routes); RegisterCustomViewEngines(ViewEngines.Engines); } <summary> This will add views from Generic </summary> <param name="viewEngines"></param> public static void RegisterCustomViewEngines(ViewEngineCollection viewEngines) { viewEngines.Clear(); //Remove this if there are complications.... viewEngines.Add(new Generic.EmbeddedResourceViewEngine()); } } } /// /// /// ///

10

Justin Jones 21st February 2013 Version 1.1 11. In the Client project, add a new controller called Home.

11

Justin Jones 21st February 2013 Version 1.1 12. In the Home controller, add an empty View called Index. Add some starter text such as Hello World.

13. Right click the Client project and select Set As Startup Project. 14. Run the project to make sure all builds ok so far.

12

Justin Jones 21st February 2013 Version 1.1 15. In the Generic project, add a controller called TestPageController and create a view for it. Leave the name as Index. Once created, add a bit of text to identify it (e.g. hello test world).

16. Right click the view, go to the properties window and SET THE VIEWS BUILD ACTION TO EMBEDDED RESOURCE! All views in the generic project must be embedded resources. If you run the project and get a View not found error it is likely the views build action property has not been set correctly.

17. In the Client project Home > Index view, create a link to the shared pages as shown below. Notice we do not need to reference Generic, the build all hooks up automatically.
@{ ViewBag.Title = "Index"; } <h2>Yatta!</h2> <br /> @* Link Text , Action, Controller ->*@ @Html.ActionLink("Test From Shared", "Index", "TestPage")

13

Justin Jones 21st February 2013 Version 1.1 18. If you try to run the project now, you will get an error as shown below:

This is because the view can no longer see the base type from the web.config. 19. Update the Generic view with a reference to MVCWebPage a format you will need to follow for all generic views .e.g.

@inherits System.Web.Mvc.WebViewPage @{ ViewBag.Title = "Index"; } <h2>Yatta!</h2>

It will now work : )

You will also be able to set a break point on the controller and debug everything as usual.

WARNING When you deploy the project, it is highly likely you will get an error when you load the project. You need to give the APP POOL you are using (full) rights to the folder (and specifically the tmp folder) of the published site.

14

Justin Jones 21st February 2013 Version 1.1

Dynamic Layout Pages


Requirement Clients/Users must be able to have specific themes. The images must not be visible to other customers. NOTE: This could be used for custom themes, browsers or types (e.g. mobile). NOTE 2: The work above will mean that all views must be Embedded Resources and will be built to a file called tmp. If you get errors about missing views, you have probably not set the file to Embedded Resource. In the Client project, if you view hidden files, you will be able to see the tmp folder and the built generic views inside. 1. In Generic > Views, add a folder called Shared (if it doesnt exist already). 2. In the shared folder, create a layout page. To do this right click Shared > Add new Item > MVC 4 Layout Page. When you name the layout page (and in fact anything in the Shared folder), the convention is to use an underscore before the name e.g. _GenericLayoutPage.cshtml.

3. Set the page as an Embedded Resource.

15

Justin Jones 21st February 2013 Version 1.1 4. Due to the embedded resource setup, the associations are broken, so you will need to add an inherits statement at the top. An example is shown below.

@inherits System.Web.Mvc.WebViewPage <!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width" /> <title>@ViewBag.Title</title> </head> <body> <div> I am in generic layout. </div> <div> @RenderBody() </div> </body> </html>

5. The LayoutPage will be called from a file called _ViewStart. ViewStart would normally contain a static reference to the layout page, but we are going to create a dynamic lookup. To do this we will need an HTML Helper. This can be extended/made classier in the future. In the Generic project, add a folder called Helpers, then a class file within that called LayoutHelper. Add the code as show below. This will be used to do a custom lookup on the layout and can be extended/altered later.

using using using using

System; System.Collections.Generic; System.Linq; System.Web;

namespace Generic.Helpers { public class LayoutHelper { public static string GetLayout() { // Default to the generic layout // Note the output will be in the tmp folder when published. string layoutName = "~/tmp/Views/Shared/_GenericLayoutPage.cshtml"; return layoutName; } } }

16

Justin Jones 21st February 2013 Version 1.1 The current file structure will look something like the screenshot below.

17

Justin Jones 21st February 2013 Version 1.1 6. We now need to create the _ViewStart page to call the layout. Right Click Views Folder > Add New Item, Select MVC 4 View Page with Layout (Razor).

7. On the next screen of the Wizard, select _GenericLayoutPage, though we will overwrite that soon.

8. Set the _ViewStart file to Embedded Resource.

18

Justin Jones 21st February 2013 Version 1.1 9. On the file created, set it to inherit from @inherits System.Web.WebPages.StartPage, add a using statement for the helper and use the Generic helper to select the layout dynamically as shown below.

@inherits System.Web.WebPages.StartPage @using Generic.Helpers @{ Layout =LayoutHelper.GetLayout(); }

10. Run the project. When you call the page in Generic now, it will call the generic layout.

19

Justin Jones 21st February 2013 Version 1.1 11. Now we will update the project to be able to switch to a client layout page. 12. In the Client project, add a folder - Views > Shared. In this, add another MVC4 Layout Page (Right click Shared Folder > Add > New Item > MVC4 Layout Page (Razor). Call it _ClientLayoutPage. Client Views/Layout Pages must NOT be set as an Embedded Resource and will not need an inherits statement.

20

Justin Jones 21st February 2013 Version 1.1 13. Add a bit of code so we can differentiate it from the Generic. An example is shown below.

14. Back in the Generic LayoutHelper file, add some code that will find the client file instead of the generic.

using using using using

System; System.Collections.Generic; System.Linq; System.Web;

namespace Generic.Helpers { public class LayoutHelper { public static string GetLayout() { // Default to the generic layout // Note the output will be in the tmp folder when published. string layoutName = "~/tmp/Views/Shared/_GenericLayoutPage.cshtml"; // If condition is met, use a different layout. if (1 == 1) // NOTE: Condition will be met. { layoutName = "~/Views/Shared/_ClientLayoutPage.cshtml"; } return layoutName; } } }

21

Justin Jones 21st February 2013 Version 1.1 15. Now run the project again, it will find the client layout is used instead of generic.

16. From the different layout pages, different CSS, Javascript and image files can be referenced. It would also allow you to package custom images specific to certain clients.
<link href="@Url.Content("~/Content/ClientSpecific.css")" rel="stylesheet" type="text/css" /> <link href="@Url.Content("~/Content/Generic1.css")" rel="stylesheet" type="text/css" /> <script src="@Url.Content("~/Scripts/jquery-X.Y.Z.min.js")" type="text/javascript"></script>

22

Justin Jones 21st February 2013 Version 1.1

Custom Attribute Setting


Requirement Clients must be able to set specific properties within their own project. For example, they may have certain flags (e.g. hide log out button) that they wish to set.

To meet this requirement should be quite simple. The client project already contains a reference to the Generic project, so properties will be easily accessible. This example will just use a Singleton and the ViewBag as a proof of concept. The Singleton is likely to be a good permanent solution, but the ViewBag could probably be improved to use a ViewModel/Strongly typed view in practice.

1. To create the singleton, go to the Generic project. Add a folder called Session. Add a class within that called SessionSingleton. Create the singleton to use the .NET session with just one property (a Boolean for proof of concept). You could have classes within the singleton we will just use a boolean for now. Copy and Paste in the code below.
using using using using System; System.Collections.Generic; System.Linq; System.Web;

namespace Generic.Session { public class SessionSingleton { public SessionSingleton() { } public static SessionSingleton _instance = new SessionSingleton(); public static SessionSingleton Instance { get { return _instance; } }

public bool IsSetFromClient { get { return (System.Web.HttpContext.Current.Session["IsSetFromClient"] != null) ? (bool)System.Web.HttpContext.Current.Session["IsSetFromClient"] : false; } set { System.Web.HttpContext.Current.Session["IsSetFromClient"] = value; } } } }

You now have an easily accessible strongly typed store. 23

Justin Jones 21st February 2013 Version 1.1 2. In our Generic TestPageController (created earlier in the demo), assign the Boolean property to a ViewBag property as shown below (viewbag is dynamic we have just created the Viewbag property on the fly). Note this could be improved with making a ViewModel or something like that. For now, we are happy to prove the concept.

using using using using using

System; System.Collections.Generic; System.Linq; System.Web; System.Web.Mvc;

namespace Generic.Controllers { public partial class TestPageController : Controller { // // GET: /TestPage/ public ActionResult Index() { ViewBag.IsSetFromClient = Generic.Session.SessionSingleton.Instance.IsSetFromClient; return View(); } } }

3. In the Generic TestPage > Index.cshtml view, add a statement to make the output obvious as to whether the item has been set or not.

@inherits System.Web.Mvc.WebViewPage @{ ViewBag.Title = "Index"; } <h2>Yatta!</h2>

@{ if (ViewBag.IsSetFromClient == true) { <p>I am set from the client : )</p> } else { <p>I am NOT SET from the client : (</p> } }

24

Justin Jones 21st February 2013 Version 1.1 4. Run the client. The output should look like the example below.

5. Now go to the client project. In the HomeController (as we know this will be hit) add some code to set the property as shown below.

using using using using using

System; System.Collections.Generic; System.Linq; System.Web; System.Web.Mvc;

namespace Client1Basic.Controllers { public class HomeController : Controller { // // GET: /Home/ public ActionResult Index() { Generic.Session.SessionSingleton.Instance.IsSetFromClient = true; return View(); } } }

6. Run the project again. It should look like the example below.

25

Justin Jones 21st February 2013 Version 1.1

Custom Routing & Overrides


Requirement Clients must be able to change functions of default functionality (e.g. have a next button go to a different page, or have custom validation).

1. This requirement can be achieved via a mix of MVC routing and inheritance. 2. In the Generic project Views/TestPage/Index.cshtml, add a using statement at the top @using System.Web.Mvc.Html

3. In the same file, add a simple form, that will post to an action. Example code below:

@inherits System.Web.Mvc.WebViewPage @using System.Web.Mvc.Html @{ ViewBag.Title = "Index"; } <h2>Yatta!</h2>

@{ if (ViewBag.IsSetFromClient == true) { <p>I am set from the client : )</p> } else { <p>I am NOT SET from the client : (</p> } }

<hr /> <!-- The HTML helper needs the using reference above.--> <!-- This code will test overriding for client specific actions.--> @using (Html.BeginForm("Submit", "TestPage")) { <input type="submit" name="btn_test" value="Test Click" /> }

26

Justin Jones 21st February 2013 Version 1.1 4. Go the Generic project TestPage controller. Add an action called Submit and a virtual string method. We are going to override the virtual method later.
using using using using using System; System.Collections.Generic; System.Linq; System.Web; System.Web.Mvc;

namespace Generic.Controllers { public partial class TestPageController : Controller { // // GET: /TestPage/ public ActionResult Index() { ViewBag.IsSetFromClient = Generic.Session.SessionSingleton.Instance.IsSetFromClient; return View(); }

/// <summary> /// This is the method for trialling overrides /// </summary> /// <returns></returns> public ActionResult Submit() { string response = BreakHereIfYouHitMe(); return RedirectToAction("Index", "Home"); }

/// <summary> /// NOTE: - this is virtual, as we are going to override it in the /// client project. /// </summary> /// <returns></returns> public virtual string BreakHereIfYouHitMe() { string test = "break here - i am generic."; return test; } } }

5. Run the project. The method BreakHereIfYouHitMe() will be hit. You can add a break point to make sure if you wish.

27

Justin Jones 21st February 2013 Version 1.1 6. In the Client project, create a controller with the same name as the one in the Generic project (i.e. TestPageController). Set it to inherit from the Generic TestPageController. Add an override for the BreakHereIfYouHitMe() method E.g.

using using using using using

System; System.Collections.Generic; System.Linq; System.Web; System.Web.Mvc;

namespace Client1Basic.Controllers { public partial class TestPageController : Generic.Controllers.TestPageController {

public override string BreakHereIfYouHitMe() { string test = "break here - i am specific to the client."; return test; } } }

28

Justin Jones 21st February 2013 Version 1.1 7. At this point, the class structure is correct, but MVC will not know the correct class to map to. To sort the routings, go to the Client Global.asax, Add a route that picks up the namespace of the client. This MUST be above the default routing in the code. The way MVC works is that it will pass through the routes until it gets what it wants. In the case there are 2 items with the same name and no preference is given in the routing, the system will error.
/// <summary> /// Standard & New Route Registration. /// </summary> /// <param name="routes"></param> public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); // NEW routes.MapRoute( "Client", // Route name "{controller}/{action}/{id}", // URL with parameters new { controller = "Home", action = "Index", id = UrlParameter.Optional },// Parameter defaults null, new string[] { "Client1Basic.Controllers" } //NOTE: namespace to check ); // STANDARD routes.MapRoute( "Default", // Route name "{controller}/{action}/{id}", // URL with parameters new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults );

8. Put a breakpoint in the client BreakHereIfYouHitMe() method and run the project. The override will now be hit. This could be used for custom navigation, validation etc.

29

Justin Jones 21st February 2013 Version 1.1

Partial Views and Strongly Typed Models


Requirement The solution must be able to support partial views.

This part of the solution will include partial views and also strongly typed models. It will not use .ascx user controls. The files to find code in Generic > Global.asax of this document would need to be updated for .ascx to work. As everything should be moved to the new format anyway, ascx files will not be considered. 1. In the Generic Project, create a class called StringContainer in the Models folder. This is just a dummy file for a strongly typed model.
using using using using System; System.Collections.Generic; System.Linq; System.Web;

namespace Generic.Models { public class StringContainer { public string Detail { get; set; } } }

2. Build the project. 3. In Generic > Views > Shared, right click and add a new View named _TestPartial. Check the box Create as Partial View and make it strongly typed to use the StringContainer class. The setup should look like the screenshot below.

30

Justin Jones 21st February 2013 Version 1.1 4. Set the Partial View to Embedded Resource. 5. The top of the file will state an @model declaration. This must be replaced with the generic inherits statement that includes a strongly typed class (due to the embedded resource not being able to see the web.config as mentioned previously). E.g.
@inherits System.Web.Mvc.WebViewPage<Generic.Models.StringContainer>

NOTE: the inherit is WebViewPage NOT ViewUserControl!!!!!! 6. Add something random to the file, so we know it has been created.
@inherits System.Web.Mvc.WebViewPage<Generic.Models.StringContainer> <p>This is all i have to say: @Model.Detail</p>

7. In the Generic Project > Views > TestPage > Index.cshtml, add an instance of the partial view, with some dummy data. E.g.
<hr /> @{ var model1 = new Generic.Models.StringContainer() { Detail = "Woo Hoo!" }; Html.RenderPartial("_TestPartial", model1); }

8. Run the project. The output should look as shown in the screenshot below.

31

Justin Jones 21st February 2013 Version 1.1

IFrame Hosting

Requirement The site must work in an iFrame.

This is just a check to ensure the solution works as expected. Further checks would be required during development. To test the solution in an iFrame. 1. Run the project on localhost and copy the URL. 2. Go to http://www.w3schools.com iframe try it yourself page (currently at http://www.w3schools.com/tags/tryit.asp?filename=tryhtml_iframe ). 3. Copy in the localhost address into the iframe src and test the site. 4. The site works as expected.

32

Justin Jones 21st February 2013 Version 1.1

Running Specific Client Scripts


Requirement The solution must enable clients to include specific JavaScript, such as a heartbeat, which can be reached across all pages. Obviously the super simple solution for this would just be for the client to have a specific layout page and include a script in that. The problem with this is that a whole new layout may be required for just one script addition. A simple strategy has been outlined below. 1. We need to use the Generic layout for this demo, so change the LayoutHelper.cs in Generic to hit the generic layout page as shown below. Here, I have just changed the condition to something that cannot be met.

33

Justin Jones 21st February 2013 Version 1.1 2. Now we need something that will dynamically pick up scripts from a project. In the Generic Helpers folder, create a class called ClientJavascriptHelper.cs. In it, copy and paste the following code. I have commented the code to explain what is going on. Basically, we are going to look for a folder called Javascript at the top level of the built project, find all the files in it and update the UI to reference them.
using using using using using System; System.IO; System.Linq; System.Text; System.Web;

namespace Generic.Helpers { public class ClientJavascriptHelper { /// <summary> /// This helper method will search for Javascript in a loaded project and /// register the scripts. /// This has not been implemented as an htmlHelper extension, as info on /// the web shows it will /// run a lot quicker. /// </summary> /// <returns></returns> public static HtmlString LoadClientJavascript() { StringBuilder clientScriptsBuilder = new StringBuilder(); // All client specific files must be placed in a folder called // "Javascript" (we can change this if we //want) DirectoryInfo directory = new DirectoryInfo(HttpContext.Current.Server.MapPath(@"~\Javascript")); // If there is no folder or no files in the folder, dont do anything if (directory == null || directory.GetFiles() == null) return new HtmlString(clientScriptsBuilder.ToString()); // Get all the files in the folder var files = directory.GetFiles().ToList(); // Loop through the files in the folder. Register each one as a script // in the page. foreach (var file in files) { string script = @"/Javascript/" + file; string jswrap = String.Format("<script type=\"text/javascript\" src=\"" + script + "\"></script>"); clientScriptsBuilder.AppendLine(jswrap); } // return the code as HTML (otherwise the Html.Encode will just output // it as text in the UI) return new HtmlString(clientScriptsBuilder.ToString()); } } }

34

Justin Jones 21st February 2013 Version 1.1 3. We now need the Generic Layout to call the method. Access Generic > Views > Shared > GenericLayoutPage.cshtml. You will need to add a using statement at the top for the helper class - @using Generic.Helpers. Then call the helper method in the appropriate place. I have followed standard convention and included it in the head tag as shown below.
@inherits System.Web.Mvc.WebViewPage @using Generic.Helpers <!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width" /> <title>@ViewBag.Title</title> @ClientJavascriptHelper.LoadClientJavascript() </head> <body> <div> I am in generic layout. </div> <div> @RenderBody() </div> </body> </html>

The Generic project is all ready to accept client specific Javascript files. 4. Run the Client project. When you navigate to TestPage, it should look normal and if you view the source code, you will see it looks as you would expect. 5. We will now set up a client with scripts to be dynamically included. Access the client project and add a new folder called Javascript at the top level. Within that, add two Javascript files (Javascript Folder > Right Click > Add > New Item > Find Javascript file > Enter the name )as shown below.

6. Open up the first script file and add something simple like:
window.onload = function () { alert("Hello from the client injected script!"); };

35

Justin Jones 21st February 2013 Version 1.1

7. Open the second script and add something that uses a different handler e.g.
window.onmousedown = function () { alert("Hello again from the client injected script!"); };

8. Run the project from client 1. When you navigate to the page that uses the Generic layout, the first message will appear.

9. Click OK on the message, and then click the screen again. The second message will appear.

10. View the source and you will see the scripts have been inserted in the head tag.

36

Justin Jones 21st February 2013 Version 1.1

MVC Areas, Area Views And Partial Views


Requirement The solution should be able to support Areas in MVC.

This example will now go through how a Generic MVC area could easily be used within the site structure. The finalised solution shown below requires little configuration to start, with everything happening automatically after the initial setup. To set the project up for MVC Areas: 1. In the Client Project, ZERO updates are required. The work has already been done when we Registered the custom view engines as shown in the screenshot below.

37

Justin Jones 21st February 2013 Version 1.1 2. In the Generic project, we need to update the EmbeddedResourceViewEngine to handle Area Views and Partial Area Views. The entire code for the file has been included below and is commented with details of what is happening.

using using using using using using

System; System.Collections.Generic; System.IO; System.Linq; System.Web; System.Web.Mvc;

namespace Generic { public class EmbeddedResourceViewEngine : RazorViewEngine { /// <summary> /// Set up the view locations (including the temp locations we will build to) /// and take the embedded views and put them in the tmp file. /// NOTE: This file should not need to be updated in for new Areas to become part of /// the solution. /// </summary> public EmbeddedResourceViewEngine() { ViewLocationFormats = new[] { "~/Views/{1}/{0}.aspx", "~/Views/{1}/{0}.ascx", "~/Views/Shared/{0}.aspx", "~/Views/Shared/{0}.ascx", "~/Views/{1}/{0}.cshtml", "~/Views/{1}/{0}.vbhtml", "~/Views/Shared/{0}.cshtml", "~/Views/Shared/{0}.vbhtml", // Improved Temp Locations // The embedded view will be copied to a temp folder // using a similar structure to the View Folder "~/tmp/Views/{1}/{0}.cshtml", "~/tmp/Views/{1}/{0}.vbhtml", }; PartialViewLocationFormats = new[] { "~/Views/Shared/{0}.cshtml", // Client first- not fully tested in this demo "~/tmp/Views/Shared/{0}.cshtml", //Improved method. - Get generic if no client }; // HANDLES ALL AREAS - Generically! AreaViewLocationFormats = new[] { "~/Areas/{2}/Views/{1}/{0}.cshtml", //client Specific Tested in this Demo! "~/tmp/Areas/{2}/Views/{1}/{0}.cshtml", //Pick up generic if no client };

AreaPartialViewLocationFormats = new[] { "~/Areas/{2}/Views/Shared/{0}.cshtml", //Client Specific not used in demo "~/tmp/Areas/{2}/Views/Shared/{0}.cshtml" //Pick up generic if no client }; // Save ALL views to the tmp file location. SaveAllViewsToTempLocation(); }

38

Justin Jones 21st February 2013 Version 1.1


/// <summary> /// Get the embedded views within the project and save the info to the tmp location. /// </summary> private static void SaveAllViewsToTempLocation() { IEnumerable<string> resources = typeof(EmbeddedResourceViewEngine).Assembly.GetManifestResourceNames().Where(name => name.EndsWith(".cshtml")); foreach (string res in resources) { SaveViewToTempLocation(res); } }

/// <summary> /// Save Resource To The Temp File. /// res will enter looking like -> Generic.Areas.TestArea.Views.Home.AreaView.cshtml /// (or something simpler), /// we need to change that to look like /// ~/tmp/Areas/TestArea/Views/Home/AreaView.cshtml /// and save it to the temp location. /// </summary> /// <param name="res"></param> private static void SaveViewToTempLocation(string res) { //Get the file path to manipulate and the fileName for re-addition later. string[] resArray = res.Split('.'); string filePath = String.Join("/", resArray, 0, resArray.Count() - 2) + "/"; string fileName = String.Join(".", resArray, resArray.Count() - 2, 2); // replace name of project, with temp file to save to. string rootPath = filePath.Replace("Generic", "~/tmp"); //Set in line with the server folder... rootPath = HttpContext.Current.Server.MapPath(rootPath); if (!Directory.Exists(rootPath)) Directory.CreateDirectory(rootPath); //Save the file to the new location. string saveToLocation = rootPath + fileName; Stream resStream = typeof(EmbeddedResourceViewEngine).Assembly.GetManifestResourceStream(res); System.Runtime.Remoting.MetadataServices.MetaData.SaveStreamToFile(resStream, saveToLocation); } } }

WARNING: Following this update, you may later get an error relating to System.Web.Optimization. If so, see Appendix B Overriding View Error for a simple resolution.

39

Justin Jones 21st February 2013 Version 1.1 3. The Generic file is now ready to handle any type of Area so lets give it a bash. Firstly, delete the two client Javascript files we created earlier (this is just because they will be annoying otherwise). Rebuild and run the solution to make sure all is well.

4. In the Generic project, right click on the project file and Add a new MVC Area (Add > Area). Call it TestArea. The first file the designer will show you is TestAreaRegistration.cs. Note: Due to the work you did in the EmbeddedResouceViewEngine, you will not need to alter this file.

5. Add a new (empty) controller called Home in the Area > TestArea > Controllers folder (Right Click > Add > Controller).

40

Justin Jones 21st February 2013 Version 1.1 6. Add an empty View to the controller (make sure the create strongly typed.. and partial checkboxes are not checked.

7. Set the View to be an Embedded Resource! 8. Add @inherits System.Web.Mvc.WebViewPage statement as with the previous generic views. Add a bit of text to identify it. The result will look like the example below.

@inherits System.Web.Mvc.WebViewPage @{ ViewBag.Title = "Index"; } <h2>I am in the area view</h2>

41

Justin Jones 21st February 2013 Version 1.1 9. Now go to the Client > Home > Index view. Add a link to the Generic Area as shown below. The main part is the ActionLink to the Area. Note the call is a standard MVC function.
@Html.ActionLink("Visit Area", "Index", "Home", new { area = "TestArea" }, null)

10. Run the project. Click the link to the area from the Home Page. You should now be able to see the page you have just created.

NOTE: In Windows Explorer, you will see the content has been copied into the tmp file of the Client Project with the appropriate structure. {TopLevelFolder}\Client1Basic\tmp\Areas\TestArea\Views\Home\Index.cshtml

42

Justin Jones 21st February 2013 Version 1.1 11. We will now test a partial view. 12. In Generic > TestArea > Views > Shared, create a partial view called _TestPartialArea.cshtml (Right Click > Add > View).

13. Set it to be an Embedded Resource and set the inherits statement (shown below) . Put some random identifying text in.

@inherits System.Web.Mvc.WebViewPage I am in the area and I'm partial

(screenshot)

43

Justin Jones 21st February 2013 Version 1.1 14. Go to the Areas > TestArea > Views > Home > Index.cshtml file. Add a using statement for @using System.Web.Mvc.Html. and a refrerence to the partial view. Note that because there is no Model in this case, we are passing the Model through as null.

@inherits System.Web.Mvc.WebViewPage @using System.Web.Mvc.Html @{ ViewBag.Title = "Index"; } <h2>I am the Area View : )</h2> @{ Html.RenderPartial("_TestPartialArea", null); }

15. Run the project. You should be able to see the partial view.

44

Justin Jones 21st February 2013 Version 1.1

MVC Area Custom Routing, Overrides and Client Specific Views


Requirement MVC Areas should be able to change/override default functionality (e.g. have a next button go to a different page, custom validation) and provide client specific views (for example, one client may have paid for custom UI controls and not want competitors to have them).

Note: In MVC 4, Controllers do not actually need to be placed in specific folders (http://www.asp.net/whitepapers/mvc4-release-notes#_Toc303253820 ). We could utilise this to avoid creating whole new Areas for minor client overrides. An example is shown in Appendix A. Note 2: Registration order of areas does not appear to be an issue in this demo, however Appendix A shows a way to force registration order of areas. For Area Routing and Overrides: 1. In Generic > Areas > TestArea > Views > Home > Index.cshtml, add a simple form with a button so we can submit to a controller. This will allow us to have a function to override. An example output is shown below, with the @using (Html.BeginForm being the bit of interest.

@inherits System.Web.Mvc.WebViewPage @using System.Web.Mvc.Html @{ ViewBag.Title = "Index"; } <h2>I am the Area View : )</h2> @{ Html.RenderPartial("_TestPartialArea", null); } <br /> <br /> @using (Html.BeginForm("Submit", "Home", new{ Area = "TestArea" })) { <input type="submit" name="btn_test" value="Test Click" /> }

45

Justin Jones 21st February 2013 Version 1.1 2. In the controller at Generic > Areas > TestArea > Controllers > HomeController.cs, add a submit function along with a virtual method we can override later.

namespace Generic.Areas.TestArea.Controllers { public class HomeController : Controller { // // GET: /TestArea/Home/ public virtual ActionResult Index() { return View(); }

/// <summary> /// This is the method for trialling overrides /// </summary> /// <returns></returns> public ActionResult Submit() { string response = BreakHereIfYoureInTheArea(); return RedirectToAction("Index", "Home", new { Area="" }); }

/// <summary> /// Note - this is virtual, as we are going to override it in the client /// project. /// </summary> /// <returns></returns> public virtual string BreakHereIfYoureInTheArea() { string test = "break here - i am generic."; return test; } } }

46

Justin Jones 21st February 2013 Version 1.1 3. Run the project to ensure the Generic function is being hit as shown below.

47

Justin Jones 21st February 2013 Version 1.1 4. We now need to head to the Client project to create the overriding controller. In it, we will override the BreakHereIfYoureInTheArea() method, a point at which custom functionality could be provided. Note: If you will only ever override controllers, you could use the method in Appendix A. The continuing solution here will be far more flexible though. 5. In the client Project, add a new Area with the same name as in the Generic project.

Area Name: TestArea

48

Justin Jones 21st February 2013 Version 1.1 6. In the client TestArea add a HomeController to mirror the one in Generic.

7. Set it to inherit from the Generic controller, so we can override methods within that controller. Comment out the Index() ActionResult. We are not going to work with this yet. The result will look like that shown below:

49

Justin Jones 21st February 2013 Version 1.1 8. Put a break point in the overriding method and Start Debugging. Go to the Visit Area link, then click the Test Click button. Your breakpoint in the override controller will be hit.

50

Justin Jones 21st February 2013 Version 1.1 9. Now we have seen we can have custom overriding within controllers, we will create a custom view that will be used instead of the generic one. Note This view could use any layout it wants and could include partial views, but we are going to make it plain for simplicity. Back in our EmbeddedResourceViewEngine (see MVC Areas, Area Views And Partial Views - step 2), we set the views to look in the client project before generic. This will make it easy to create an overriding view or partial view.

10. Although not necessary, if we wanted to override the ActionResult Index() itself, this could be done by making the Generic controller virtual and the client override it as shown in the screenshots below.

NOTE: If you have two Index() methods with no preference (virtual/override), MVC not know which to use and provide the following error: The current request for action on controller type is ambiguous between the following action methods) as it wont know which to use.

51

Justin Jones 21st February 2013 Version 1.1 11. To create an overriding view, uncomment the Clients Index() ActionResult (if it is still commented out) and add a View. This will NOT need to be an embedded resource.

12. Give it the standard settings

13. In the view, add some text so we can easily identify it.

52

Justin Jones 21st February 2013 Version 1.1 14. Comment out the Clients ActionResult to use the one from Generic (you could keep it in to create a completely custom override, but for the purpose of this demo, comment it out).

15. If you run the project, and click on the Visit Area link again, you will now be taken to the overriding view. WARNING: If you get an error here (System.Web.Optimization), see Appendix B Overriding View Error for a simple resolution.

53

Justin Jones 21st February 2013 Version 1.1 16. Here, the new view does not contain a submit button. For the sake of completeness, we will add it now. The button could submit to the generic controller, or an override within the client controller. 17. Add a submit button to your client override view as you had done in the generic view. The result will look like the screenshot below:

@{ ViewBag.Title = "Index"; } <h2>I am an overriding view!</h2> <br /> <br /> @using (Html.BeginForm("Submit", "Home", new{ Area = "TestArea" })) { <input type="submit" name="btn_test" value="Test Click" /> }

54

Justin Jones 21st February 2013 Version 1.1 18. Put a breakpoint in the Generic Submit() ActionResult and run the project. When you go to Visit Area and click the test button, you will be able to access the Generic controllers submit action.

As you can see, the structure allows you to mix and match between client and generic functionality as required.

55

Justin Jones 21st February 2013 Version 1.1

*** END OF DEMO ***

56

Justin Jones 21st February 2013 Version 1.1

Appendix A Using MVC 4 Functionality to Override Area Controllers Only


If you will never have overriding views, you could create your override structure in a neater way than creating a whole new area. It is highly unlikely, but this section would continue from part 4 of MVC Area Custom Routing and Overrides.

1. In the Client project, add a folder called Areas, then under that a folder called TestArea, then a folder under that called Controllers (i.e. matching the area structure in Generic) .In that, add a controller called HomeController.cs. The result should look like the structure below.

2. In the client home controller file, set it to inherit from the Generic Area Home Controller and override the BreakHereIfYoureInTheArea() method. The result should look like the code shown below.
using using using using using System; System.Collections.Generic; System.Linq; System.Web; System.Web.Mvc;

namespace Client1Basic.Areas.TestArea.Controllers { public class HomeController : Generic.Areas.TestArea.Controllers.HomeController { /// <summary> /// This is just an override so we can break here and know we are hitting /// the override controller, /// not the generic one. /// </summary> /// <returns></returns> public override string BreakHereIfYoureInTheArea() { string test = "break here - i am sneaking into the area from the client."; return test; } } }

57

Justin Jones 21st February 2013 Version 1.1 3. We now need to update the routing so the Client override controller will be hit instead of the Generic. A bit of extra work is required here, as standard Area registration runs in an unpredictable order when using this method. The basic concept is that we are going to create a new AreaRegistrationContext for the Client Area (which will relate to the standard routing RouteTable.Routes), and then add our Client Area mapping to it (ahead of the standard Area registration). If we were to do this for another Area, we would need to create a new AreaRegistrationContext and another mapping. In future, this sort of functionality could use reflection to look up the Area/Namespace, or more simply have a dictionary of AreaName/Namespaces and loop through creating the mappings.

4. In the Client Global.asax, add a new method called RegisterAreaOverrides() below RegisterRoutes(). The code is shown below.

/// <summary> /// This method will register areas in a custom manner, so we can ensure the order /// of registration is correct. /// This could be moved to a more generic method with a loop, but has been kept /// simple for ease of reading. /// </summary> public void RegisterAreaOverrides() { // Important - The area name in the registration context must be the name of // the area to override. // Important - A new context must be created for each different area. AreaRegistrationContext context = new AreaRegistrationContext("TestArea", RouteTable.Routes); //Map the override route. context.MapRoute( "TestArea_override", //Just a unique key "TestArea/{controller}/{action}/{id}", //Used 'TestArea/' // as this is the area the routing will search for. new { action = "Index", controller="Home", id = UrlParameter.Optional }, null, new string[] { "Client1Basic.Areas.TestArea.Controllers" } //Namespace of the overriding controller (above) ); }

58

Justin Jones 21st February 2013 Version 1.1 5. We now need to ensure this is called before the standard area registration. To do this, call RegisterAreaOverrides() above AreaRegistration.RegisterAllAreas() in Application_Start() as shown in the example below.

/// <summary> /// standard application start, with additional code to call generic. /// </summary> protected void Application_Start() { // Register the overrides before the areas to ensure the routing is // correct! RegisterAreaOverrides(); // This standard function will still register all non-overridden areas, // but they will be the fallback, giving the overrides precedence. AreaRegistration.RegisterAllAreas(); RegisterGlobalFilters(GlobalFilters.Filters); RegisterRoutes(RouteTable.Routes); RegisterCustomViewEngines(ViewEngines.Engines); }

6. Put a breakpoint in the overriding controller and test the Area Page. When you click the button, the override method will be hit.

59

Justin Jones 21st February 2013 Version 1.1

Appendix B Overriding View Error


WARNING: If you have overriding views, you may get the following error:
Could not load file or assembly 'System.Web.Optimization, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35' or one of its dependencies. The system cannot find the file specified.

If you do, in visual studio, go to Tools > Library Package Manager > Package Manager Console and enter the following command
PM> Install-Package Microsoft.Web.Optimization -Pre

More details can be found at http://stackoverflow.com/questions/11590822/getting-system-weboptimization-to-work-in-a-razor-view-within-a-class-library

60

Justin Jones 21st February 2013 Version 1.1

Resources
Some of the main resources used are listed below. 1. http://stackoverflow.com/questions/4800819/sharing-controllers-and-views-with-multipleweb-applications/11629904#11629904 2. http://stackoverflow.com/questions/8127462/the-view-must-derive-from-webviewpage-orwebviewpagetmodel 3. http://www.picnet.com.au/blogs/Guido/post/2009/08/12/Sharing-MVC-Views-AcrossProjects 4. http://www.asp.net/mvc/tutorials/controllers-and-routing/creating-custom-routes-cs 5. http://www.asp.net/mvc/tutorials/older-versions/views/creating-custom-html-helpers-cs 6. http://msdn.microsoft.com/en-us/vs11trainingcourse_aspnetmvc4.aspx 7. http://vimeo.com/9217399 8. http://oredev.org/2010/sessions/pluggable-web-applications-with-asp-net-mvc 9. http://lonetechie.com/2012/09/25/multi-tenant-architecture-with-asp-net-mvc-4/ 10. http://www.asp.net/whitepapers/mvc4-release-notes#_Toc303253820 11. http://stackoverflow.com/questions/11590822/getting-system-web-optimization-to-workin-a-razor-view-within-a-class-library

61

Você também pode gostar