2 razor partial views with the same name in differ

2019-05-27 11:24发布

问题:

In our project we're using the razorgenerator of David Ebbo. This allowed us to move some of our cshtml files to a class library.

What we would like to achieve now is the following:

  • MyCommonViews has a "MyView.cshtml" in its Views folder.
  • MyWebProject ALSO has a "MyView.cshtml" in its Views folder.
  • MyOtherWebProject DOES NOT have a "MyView.cshtml" in its Views folder.

When MyOtherWebProject needs to load MyView.cshtml, it will pick the one which is in the compiled MyCommonViews project. That is what we want.
BUT when MyWebProject needs to load MyView.cshtml, we would like it to pick up the "overridden" MyView.cshtml file which is in the MyWebProject itself.

Is what we want possible and how?

Manu.

回答1:

There is flag PreemptPhysicalFiles = false which does the magic.

Full sample:

[assembly: WebActivator.PostApplicationStartMethod(typeof(Application.Web.Common.App_Start.RazorGeneratorMvcStart), "Start")]

namespace Application.Web.Common.App_Start
{
    public static class RazorGeneratorMvcStart
    {
        public static void Start()
        {
            var engine = new PrecompiledMvcEngine2(typeof (RazorGeneratorMvcStart).Assembly)
                             {
                                 UsePhysicalViewsIfNewer = true, //compile if file changed
                                 PreemptPhysicalFiles = false //use local file if exist
                             };

            ViewEngines.Engines.Add(engine);//Insert(0,engine) ignores local partial views

            // StartPage lookups are done by WebPages. 
            VirtualPathFactoryManager.RegisterVirtualPathFactory(engine);
        }
    }
}

However there is maybe a small bug: http://razorgenerator.codeplex.com/workitem/100



回答2:

I wrote up a hacky solution for our problem. It hacks into the razorgenerators viewengine and removes all appropriate entries from the (private readonly) Dictionary it has.
The code is ran on application start.

Talk is cheap, show me the code:

    private static void HackRazorGeneratorToAllowViewOverriding()
    {
        // first we search for the PrecompiledMvcEngine
        var razorGeneratorViewEngine = ViewEngines.Engines.ToList().FirstOrDefault(ve => ve.GetType().Name.Contains("PrecompiledMvcEngine"));
        if (razorGeneratorViewEngine == null)
            return;

        // retrieve the dictionary where it keeps the mapping between a view path and the (view object) type to instantiate
        var razorMappings = (IDictionary<string, Type>)ReflectionUtils.GetPrivateReadonly("_mappings", razorGeneratorViewEngine);

        // retrieve a list of all our cshtml files in our 'concrete' web project
        var files = Directory.GetFiles(Path.Combine(WebConfigSettings.RootPath, "Views"), "*.cshtml", SearchOption.AllDirectories);

        // do some kungfu on those file paths so that they are in the same format as in the razor mapping dictionary
        var concreteViewPaths = files.Select(fp => string.Format("~{0}", fp.Replace(WebConfigSettings.RootPath, "").Replace(@"\", "/"))).ToList();

        // loop through each of the cshtml paths (of our 'concrete' project) and remove it from the razor mappings if it's there
        concreteViewPaths.ForEach(vp =>
                                      {
                                          if (razorMappings.ContainsKey(vp))
                                              razorMappings.Remove(vp);
                                      });
    }

WebConfigSettings.RootPath contains the path on HD to the root of our web application.

This is a part of our static ReflectionUtils class:

/// <summary>
/// Get a field that is 'private readonly'.
/// </summary>
public static object GetPrivateReadonly(string readonlyPropName, object instance)
{
    var field = instance.GetType().GetField(readonlyPropName, BindingFlags.Instance | BindingFlags.NonPublic);
    if (field == null)
        throw new NullReferenceException(string.Format("private readonly field '{0}' not found in '{1}'", readonlyPropName, instance));
    return field.GetValue(instance);
}

This did the trick. We basically force the PrecompiledMvcEngine to "forget" any view which we have in our concrete project.



回答3:

You can also try CompositePrecompiledMvcEngine from RazorGenerator.Mvc 2.1.0. It was designed for correct support of view overriding within multiple assemblies. Piece of code:

var engine = new CompositePrecompiledMvcEngine(
/*1*/ PrecompiledViewAssembly.OfType<MyCommonViewsSomeClass>(),
/*2*/ PrecompiledViewAssembly.OfType<MyWebProjectSomeClass>(
        usePhysicalViewsIfNewer: HttpContext.Current.IsDebuggingEnabled));

ViewEngines.Engines.Insert(0, engine);
VirtualPathFactoryManager.RegisterVirtualPathFactory(engine);

The first line will register all views from the MyCommonViews assembly (~/Views/MyView.cshtml), the second line will register all views from the MyWebProject or MyOtherWebProject assembly.

When it encounters the virtual path, that already has been registered (~/Views/MyView.cshtml from the MyWebProject assembly), it overrides an old mapping with a new view type mapping.

If another project doesn't has view with the same virtual path (MyOtherWebProject) it leaves source mapping unchanged.