¿Puedo especificar una ubicación personalizada para "buscar vistas" en ASP.NET MVC?

105

Tengo el siguiente diseño para mi proyecto mvc:

  • / Controladores
    • /Manifestación
    • / Demo / DemoArea1Controller
    • / Demo / DemoArea2Controller
    • etc ...
  • /Puntos de vista
    • /Manifestación
    • /Demo/DemoArea1/Index.aspx
    • /Demo/DemoArea2/Index.aspx

Sin embargo, cuando tengo esto para DemoArea1Controller:

public class DemoArea1Controller : Controller
{
    public ActionResult Index()
    {
        return View();
    }
}

Aparece el error "No se pudo encontrar la vista 'índice' o su maestro", con las ubicaciones de búsqueda habituales.

¿Cómo puedo especificar los controladores en la búsqueda del espacio de nombres "Demo" en la subcarpeta de vista "Demo"?

Daniel Schaffer
fuente
Aquí hay otra muestra de un ViewEngine simple de la aplicación MVC Commerce de Rob Connery: Ver el código del motor y el código Global.asax.cs para configurar ViewEngine: Global.asax.cs Espero que esto ayude.
Robert Dean

Respuestas:

121

Puede ampliar fácilmente WebFormViewEngine para especificar todas las ubicaciones en las que desea buscar:

public class CustomViewEngine : WebFormViewEngine
{
    public CustomViewEngine()
    {
        var viewLocations =  new[] {  
            "~/Views/{1}/{0}.aspx",  
            "~/Views/{1}/{0}.ascx",  
            "~/Views/Shared/{0}.aspx",  
            "~/Views/Shared/{0}.ascx",  
            "~/AnotherPath/Views/{0}.ascx"
            // etc
        };

        this.PartialViewLocationFormats = viewLocations;
        this.ViewLocationFormats = viewLocations;
    }
}

Asegúrese de recordar registrar el motor de visualización modificando el método Application_Start en su Global.asax.cs

protected void Application_Start()
{
    ViewEngines.Engines.Clear();
    ViewEngines.Engines.Add(new CustomViewEngine());
}
Sam Wessel
fuente
¿Cómo se puede acceder a la ruta de una página maestra desde una página maestra anidada? Como en la configuración del diseño de la página maestra anidada para buscar dentro de las rutas de CustomViewEngine
Drahcir
6
¿No es mejor si omitimos Borrar los motores ya registrados y simplemente agregamos el nuevo y viewLocations solo tendrá los nuevos?
Prasanna
3
Implementa sin ViewEngines.Engines.Clear (); Todo funciona bien. Si desea utilizar * .cshtml, debe heredar de RazorViewEngine
KregHEk
¿Hay alguna forma de vincular las opciones "agregar vista" e "ir a ver" de los controladores a las nuevas ubicaciones de vista? Estoy usando Visual Studio 2012
Neville Nazerane
Como lo menciona @Prasanna, no es necesario borrar los motores existentes para agregar nuevas ubicaciones, consulte esta respuesta para obtener más detalles.
Hooman Bahreini
44

Ahora en MVC 6 puede implementar la IViewLocationExpanderinterfaz sin perder el tiempo con los motores de vista:

public class MyViewLocationExpander : IViewLocationExpander
{
    public void PopulateValues(ViewLocationExpanderContext context) {}

    public IEnumerable<string> ExpandViewLocations(ViewLocationExpanderContext context, IEnumerable<string> viewLocations)
    {
        return new[]
        {
            "/AnotherPath/Views/{1}/{0}.cshtml",
            "/AnotherPath/Views/Shared/{0}.cshtml"
        }; // add `.Union(viewLocations)` to add default locations
    }
}

donde {0}es el nombre de la vista de destino, {1}- nombre del controlador y {2}- nombre del área.

Puede devolver su propia lista de ubicaciones, fusionarla con la predeterminada viewLocations( .Union(viewLocations)) o simplemente cambiarlas ( viewLocations.Select(path => "/AnotherPath" + path)).

Para registrar su expansor de ubicación de vista personalizada en MVC, agregue las siguientes líneas al ConfigureServicesmétodo en el Startup.csarchivo:

public void ConfigureServices(IServiceCollection services)
{
    services.Configure<RazorViewEngineOptions>(options =>
    {
        options.ViewLocationExpanders.Add(new MyViewLocationExpander());
    });
}
whyleee
fuente
3
Ojalá pudiera votar esto por 10 votos. Es exactamente lo que se necesita en Asp.net 5 / MVC 6. Hermoso. Muy útil en mi caso (y en otros) cuando desea agrupar áreas en superáreas para sitios más grandes o agrupaciones lógicas.
drewid
La porción Startup.cs debe ser: services.Configure <RazorViewEngineOptions> Va en este método: public void ConfigureServices (IServiceCollection services)
OrangeKing89
42

En realidad, hay un método mucho más fácil que codificar las rutas en su constructor. A continuación se muestra un ejemplo de cómo extender el motor Razor para agregar nuevas rutas. Una cosa de la que no estoy completamente seguro es si las rutas que agregue aquí se almacenarán en caché:

public class ExtendedRazorViewEngine : RazorViewEngine
{
    public void AddViewLocationFormat(string paths)
    {
        List<string> existingPaths = new List<string>(ViewLocationFormats);
        existingPaths.Add(paths);

        ViewLocationFormats = existingPaths.ToArray();
    }

    public void AddPartialViewLocationFormat(string paths)
    {
        List<string> existingPaths = new List<string>(PartialViewLocationFormats);
        existingPaths.Add(paths);

        PartialViewLocationFormats = existingPaths.ToArray();
    }
}

Y tu Global.asax.cs

protected void Application_Start()
{
    ViewEngines.Engines.Clear();

    ExtendedRazorViewEngine engine = new ExtendedRazorViewEngine();
    engine.AddViewLocationFormat("~/MyThemes/{1}/{0}.cshtml");
    engine.AddViewLocationFormat("~/MyThemes/{1}/{0}.vbhtml");

    // Add a shared location too, as the lines above are controller specific
    engine.AddPartialViewLocationFormat("~/MyThemes/{0}.cshtml");
    engine.AddPartialViewLocationFormat("~/MyThemes/{0}.vbhtml");

    ViewEngines.Engines.Add(engine);

    AreaRegistration.RegisterAllAreas();
    RegisterRoutes(RouteTable.Routes);
}

Una cosa a tener en cuenta: su ubicación personalizada necesitará el archivo ViewStart.cshtml en su raíz.

Chris S
fuente
23

Si solo desea agregar nuevas rutas, puede agregar a los motores de vista predeterminados y ahorrar algunas líneas de código:

ViewEngines.Engines.Clear();
var razorEngine = new RazorViewEngine();
razorEngine.MasterLocationFormats = razorEngine.MasterLocationFormats
      .Concat(new[] { 
          "~/custom/path/{0}.cshtml" 
      }).ToArray();

razorEngine.PartialViewLocationFormats = razorEngine.PartialViewLocationFormats
      .Concat(new[] { 
          "~/custom/path/{1}/{0}.cshtml",   // {1} = controller name
          "~/custom/path/Shared/{0}.cshtml" 
      }).ToArray();

ViewEngines.Engines.Add(razorEngine);

Lo mismo se aplica a WebFormEngine

Marcelo De Zen
fuente
2
Para Vistas: use razorEngine.ViewLocationFormats.
Aldentev
13

En lugar de subclasificar RazorViewEngine, o reemplazarlo directamente, puede simplemente alterar la propiedad PartialViewLocationFormats existente de RazorViewEngine. Este código va en Application_Start:

System.Web.Mvc.RazorViewEngine rve = (RazorViewEngine)ViewEngines.Engines
  .Where(e=>e.GetType()==typeof(RazorViewEngine))
  .FirstOrDefault();

string[] additionalPartialViewLocations = new[] { 
  "~/Views/[YourCustomPathHere]"
};

if(rve!=null)
{
  rve.PartialViewLocationFormats = rve.PartialViewLocationFormats
    .Union( additionalPartialViewLocations )
    .ToArray();
}
Simon Giles
fuente
2
Esto funcionó para mí, con la excepción de que el tipo de motor de la maquinilla de afeitar era 'FixedRazorViewEngine' en lugar de 'RazorViewEngine'. También lanzo una excepción si no se encontró el motor, ya que evita que mi aplicación se inicialice con éxito.
Rob
3

La última vez que verifiqué, esto requiere que cree su propio ViewEngine. Sin embargo, no sé si lo hicieron más fácil en RC1.

El enfoque básico que utilicé antes del primer RC fue, en mi propio ViewEngine, dividir el espacio de nombres del controlador y buscar carpetas que coincidieran con las partes.

EDITAR:

Regresé y encontré el código. Esta es la idea general.

public override ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName)
{
    string ns = controllerContext.Controller.GetType().Namespace;
    string controller = controllerContext.Controller.GetType().Name.Replace("Controller", "");

    //try to find the view
    string rel = "~/Views/" +
        (
            ns == baseControllerNamespace ? "" :
            ns.Substring(baseControllerNamespace.Length + 1).Replace(".", "/") + "/"
        )
        + controller;
    string[] pathsToSearch = new string[]{
        rel+"/"+viewName+".aspx",
        rel+"/"+viewName+".ascx"
    };

    string viewPath = null;
    foreach (var path in pathsToSearch)
    {
        if (this.VirtualPathProvider.FileExists(path))
        {
            viewPath = path;
            break;
        }
    }

    if (viewPath != null)
    {
        string masterPath = null;

        //try find the master
        if (!string.IsNullOrEmpty(masterName))
        {

            string[] masterPathsToSearch = new string[]{
                rel+"/"+masterName+".master",
                "~/Views/"+ controller +"/"+ masterName+".master",
                "~/Views/Shared/"+ masterName+".master"
            };


            foreach (var path in masterPathsToSearch)
            {
                if (this.VirtualPathProvider.FileExists(path))
                {
                    masterPath = path;
                    break;
                }
            }
        }

        if (string.IsNullOrEmpty(masterName) || masterPath != null)
        {
            return new ViewEngineResult(
                this.CreateView(controllerContext, viewPath, masterPath), this);
        }
    }

    //try default implementation
    var result = base.FindView(controllerContext, viewName, masterName);
    if (result.View == null)
    {
        //add the location searched
        return new ViewEngineResult(pathsToSearch);
    }
    return result;
}
Joel
fuente
1
De hecho, es mucho más fácil. Subclase WebFormsViewEngine y luego simplemente agréguelo a la matriz de rutas que ya busca en su constructor.
Craig Stuntz
Bueno saber. La última vez que necesité modificar esa colección, no fue posible de esa manera.
Joel
Vale la pena mencionar que debe establecer la variable "baseControllerNamespace" en el espacio de nombres de su controlador base (por ejemplo, "Project.Controllers"), pero por lo demás hice exactamente lo que necesitaba, 7 años después de ser publicado.
prototipo14
3

Intente algo como esto:

private static void RegisterViewEngines(ICollection<IViewEngine> engines)
{
    engines.Add(new WebFormViewEngine
    {
        MasterLocationFormats = new[] {"~/App/Views/Admin/{0}.master"},
        PartialViewLocationFormats = new[] {"~/App/Views/Admin//{1}/{0}.ascx"},
        ViewLocationFormats = new[] {"~/App/Views/Admin//{1}/{0}.aspx"}
    });
}

protected void Application_Start()
{
    RegisterViewEngines(ViewEngines.Engines);
}
Vitaliy Ulantikov
fuente
3

Nota: para ASP.NET MVC 2 tienen rutas de ubicación adicionales que deberá configurar para las vistas en 'Áreas'.

 AreaViewLocationFormats
 AreaPartialViewLocationFormats
 AreaMasterLocationFormats

La creación de un motor de visualización para un área se describe en el blog de Phil .

Nota: Esto es para la versión preliminar 1, por lo que está sujeto a cambios.

Simon_Weaver
fuente
1

La mayoría de las respuestas aquí, borre las ubicaciones existentes llamando ViewEngines.Engines.Clear()y luego agréguelas nuevamente ... no es necesario hacer esto.

Simplemente podemos agregar las nuevas ubicaciones a las existentes, como se muestra a continuación:

// note that the base class is RazorViewEngine, NOT WebFormViewEngine
public class ExpandedViewEngine : RazorViewEngine
{
    public ExpandedViewEngine()
    {
        var customViewSubfolders = new[] 
        {
            // {1} is conroller name, {0} is action name
            "~/Areas/AreaName/Views/Subfolder1/{1}/{0}.cshtml",
            "~/Areas/AreaName/Views/Subfolder1/Shared/{0}.cshtml"
        };

        var customPartialViewSubfolders = new[] 
        {
            "~/Areas/MyAreaName/Views/Subfolder1/{1}/Partials/{0}.cshtml",
            "~/Areas/MyAreaName/Views/Subfolder1/Shared/Partials/{0}.cshtml"
        };

        ViewLocationFormats = ViewLocationFormats.Union(customViewSubfolders).ToArray();
        PartialViewLocationFormats = PartialViewLocationFormats.Union(customPartialViewSubfolders).ToArray();

        // use the following if you want to extend the master locations
        // MasterLocationFormats = MasterLocationFormats.Union(new[] { "new master location" }).ToArray();   
    }
}

Ahora puede configurar su proyecto para usar lo anterior RazorViewEngineen Global.asax:

protected void Application_Start()
{
    ViewEngines.Engines.Add(new ExpandedViewEngine());
    // more configurations
}

Consulte este tutorial para obtener más información.

Hooman Bahreini
fuente