Hosting ASP.Net Mvc in SharePoint

In Office SharePoint Server 2007 & Windows SharePoint Services 3

1. Enabling ASP.Net 3.5

  • Start Visual Studio 2008.
  • Create a new dummy ASP.NET Web Application Project, and make sure you target the .NET Framework 2.0 (upper right dropdown of the New Project dialog). The name of this project is not important; you won’t need it anymore when we’re done.
  • Copy the web.config of your SharePoint 2007 site, into the dummy Web Application project in Visual Studio.
  • Open the Project Properties in Visual Studio (right click on the Project node in the Solution Explorer, and choose Properties; or in the Project menu, select WebApplicationX Properties).
  • Select .NET Framework 3.5 in the Target Framework dropdown (select Yes in the confirmation dialog).
  • Copy the web.config from the Web Application Project back to SharePoint.
  • If you want to use <ScriptManager> in SharePoint MasterPage,you need to modify the web.config,add node in <SafeControls>.
<SafeControls>
    <SafeControl Assembly="System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" Namespace="System.Web.UI" TypeName="*" Safe="True" />
</SafeControls>
  • Register Routes and ASP.Net Mvc

2. URL Routing

  • Modify the web.config of your sharepoint site,add nodes like bellow:
<configuration>
  <system.web>
    <httpHandlers>
      <add verb="*" path="*.mvc" validate="false" type="System.Web.Mvc.MvcHttpHandler, System.Web.Mvc, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
    </httpHandlers>
    <httpModules>
      <add name="UrlRoutingModule" type="System.Web.Routing.UrlRoutingModule, System.Web.Routing, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
    </httpModules>
    <compilation batch="false" debug="false">
      <assemblies>
        <add assembly="System.Web.Routing, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
        <add assembly="System.Web.Mvc, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
      </assemblies>
    </compilation>
    <pages enableSessionState="false" enableViewState="true" enableViewStateMac="true" validateRequest="false" 
           pageParserFilterType="Microsoft.SharePoint.ApplicationRuntime.SPPageParserFilter, Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" 
           asyncTimeout="7">
      <namespaces>
        <add namespace="System.Web.Mvc"/>
        <add namespace="System.Web.Mvc.Ajax"/>
        <add namespace="System.Web.Mvc.Html"/>
        <add namespace="System.Web.Routing"/>
        <add namespace="System.Linq"/>
        <add namespace="System.Collections.Generic"/>
      </namespaces>
    </pages>
  </system.web>
  <system.webServer>
    <handlers>
      <remove name="MvcHttpHandler"/>
      <remove name="UrlRoutingHandler"/>
      <add name="MvcHttpHandler" preCondition="integratedMode" verb="*" path="*.mvc" 
           type="System.Web.Mvc.MvcHttpHandler, System.Web.Mvc, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
      <add name="UrlRoutingHandler" preCondition="integratedMode" verb="*" path="UrlRouting.axd" 
           type="System.Web.HttpForbiddenHandler, System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"/>
    </handlers>
  </system.webServer>
</configuration>
Ok,if you see a error page, then you can see real error description and callstack/stack trace like below:

[ArgumentException: virtualPath]
   Microsoft.SharePoint.ApplicationRuntime.SPRequestModule.IsExcludedPath(String virtualPath) +486
   Microsoft.SharePoint.ApplicationRuntime.SPVirtualPathProvider.FileExists(String virtualPath) +188
   System.Web.Routing.RouteCollection.GetRouteData(HttpContextBase httpContext) +148
   System.Web.Routing.UrlRoutingModule.PostResolveRequestCache(HttpContextBase context) +34
   System.Web.SyncEventExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute() +80
   System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously) +171
Then,Reflecting the System.Web.Routing.

RouteCollection Constructor

public RouteCollection() : this(HostingEnvironment.VirtualPathProvider)
{
}

public RouteCollection(VirtualPathProvider virtualPathProvider)
{
    this._namedMap = new Dictionary<string, RouteBase>(StringComparer.OrdinalIgnoreCase);
    this._rwLock = new ReaderWriterLock();
    this._vpp = virtualPathProvider;
}
RouteCollection.GetRouteData Method

    public RouteData GetRouteData(HttpContextBase httpContext)
    {
        // other codes...

        if (!this.RouteExistingFiles)
        {
            string appRelativeCurrentExecutionFilePath = httpContext.Request.AppRelativeCurrentExecutionFilePath;
            if (((appRelativeCurrentExecutionFilePath != "~/") 
                && (this._vpp != null)) 
                && (this._vpp.FileExists(appRelativeCurrentExecutionFilePath) 
                || this._vpp.DirectoryExists(appRelativeCurrentExecutionFilePath)))
            {
                return null;
            }
        }

        // other codes...
    }
After Reflecting the issue, as we know HttpRequest.AppRelativeCurrentExecutionFilePath Property maybe start with "~".But it turns out that WSS own VirtualPathProvider (SPVirtualPathProvider, briefly described by this MSDN page) refuses to handle requests for which the virtual path starts with "~".
So where cause this.
  • Custom MvcVirtualPathProvider
    /// <summary>
    ///  Filtrate the virtual path when that starts with '~' for SPVirtualPathProvider. 
    /// </summary>
    public class MvcVirtualPathProvider : VirtualPathProvider
    {
        public override string CombineVirtualPaths(string basePath, string relativePath)
        {
            return Previous.CombineVirtualPaths(basePath, relativePath); ;
        }

        public override bool DirectoryExists(string virtualDir)
        {
            if (virtualDir.StartsWith("~/"))
            {
                virtualDir = virtualDir.Remove(0, 1);
            }

            return Previous.DirectoryExists(virtualDir);
        }

        public override bool FileExists(string virtualPath)
        {
            if (virtualPath.StartsWith("~/"))
            {
                virtualPath = virtualPath.Remove(0, 1);
            }

            return Previous.FileExists(virtualPath);
        }
    }
You can get more info at SharePoint 2007 as a WCF host - Step #4, Write a Virtual Path Provider,but don't register the MvcVirtualPathProvider in HttpModule.I'll explane why bellow.
  • Register VirtualPathProvider and Routes
In ASP.Net Mvc,if you use custom VirtualPathProvider to retrieve resources from a virtual file system in you site,you must register the custom VirtualPathProvider in the global.asax.You can get more info form ASP.NET MVC Plugins.
So:
    /// <summary>
    /// Register SPVirtualPathProvider,MvcVirtualPathProvider and Routes
    /// </summary>
    public class MvcHttpApplication : SPHttpApplication
    {
        public static void RegisterVirtualPathProviders()
        {
            //Reflection the SPVirtualPathProvider
            Assembly assembly = Assembly.Load("Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c");
            Type type = assembly.GetType("Microsoft.SharePoint.ApplicationRuntime.SPVirtualPathProvider");
            Object obj = assembly.CreateInstance("Microsoft.SharePoint.ApplicationRuntime.SPVirtualPathProvider");

            HostingEnvironment.RegisterVirtualPathProvider((VirtualPathProvider)obj);
            HostingEnvironment.RegisterVirtualPathProvider(new MvcVirtualPathProvider());
        }

        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 = "" }      // Parameter defaults
            );
        }

        protected void Application_Start()
        {
            RegisterVirtualPathProviders();
            RegisterRoutes(RouteTable.Routes);
            //RouteDebug.RouteDebugger.RewriteRoutesForTesting(RouteTable.Routes);
            //RouteTable.Routes.RouteExistingFiles = false;
        }
    }
Now put both of the above in the GAC (I put them both as 2 classes in a single assembly called SimOne.SharePoint.Solutions). You can download the source code
  • Modify the global.asax
<%@ Assembly Name="Microsoft.SharePoint" %>
<%@ Assembly Name="SimOne.SharePoint.Solutions" %>
<%@ Import Namespace="SimOne.SharePoint.ApplicationRuntime" %>
<%@ Application Language="C#" Inherits="SimOne.SharePoint.ApplicationRuntime.MvcHttpApplication" %>

Now,refresh your sharepoint site index page (like:http://yourdomain/default.aspx) and others pages,if it looks fine,Congratulations :-) Congratulations! It hit half.

3. Views,MasterPage,ViewEngine

  • Where the Views to put?
Put your ASP.Net Mvc Project's Content,Scripts,Views folders to the root of your sharepoint site by SharePoint Designer, and I'm Sorry for the designer won't work,and you should do this at first. :-P.
If you want to cancle routing.
  1. In global.asax,return to <%@ Application Language="C#" Inherits="Microsoft.SharePoint.ApplicationRuntime.SPHttpApplication" %>
  2. In web.config,return to <!--<add name="UrlRoutingModule" type="System.Web.Routing.UrlRoutingModule, System.Web.Routing, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>-->
Now,you can use SharePoint Designer as usual.

Modify the web.config,add the nodes like bellow:
<configuration>
  <SharePoint>
    <SafeMode MaxControls="200" CallStack="true" DirectFileDependencies="10" TotalFileDependencies="50" AllowPageLevelTrace="false">
      <PageParserPaths>
        <PageParserPath VirtualPath="/Views/*" CompilationMode="Always" AllowServerSideScript="true" IncludeSubFolders="true"/>
      </PageParserPaths>
    </SafeMode>
    <SafeControls>
      <SafeControl Assembly="System.Web.Mvc, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" Namespace="System.Web.Mvc" TypeName="ViewMasterPage" Safe="True" />
      <SafeControl Assembly="System.Web.Mvc, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" Namespace="System.Web.Mvc" TypeName="ViewPage" Safe="True" />
      <SafeControl Assembly="System.Web.Mvc, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" Namespace="System.Web.Mvc" TypeName="ViewUserControl" Safe="True" />
    </SafeControls>
  </SharePoint>
</configuration>
Visit your sharepoint site again,you will find your ASP.Net Mvc project look fine in SharePoint.

In Office SharePoint Server 2010 & SharePoint Foundation 2010 (Windows SharePoint Services 2010)

TO BE CONTINUE.

I'll talk about the masterpage and ViewEngine later,Then you can see the ASP.Net Mvc project can use the sharepoint default masterpage.

Home/Index
Home/About
Sorry for my poor english. I'll record a video for this.Wish this essay will help you.if you have any idea about how to host ASP.Net Mvc in SharePoint,you can leave your word.

Last edited Jan 5, 2010 at 4:12 PM by WindBell, version 33

Comments

dkrants Jun 11, 2011 at 5:03 AM 
Can you please describe how you've made your MVC app use SharePoint's Master Page,

dolan May 7, 2010 at 6:08 PM 
nice work! using the virtual path provider approach, I wouldn't have even thought of that.

lecamarade Apr 30, 2010 at 3:06 PM 
Thanks for the article.

Can you please post the final version of your sharepoint web.config file?
Do you keep the web.config form the web project?