Failover to an alternate View when Partial View is not found?
Asked Answered
W

4

5

I have an MVC app that uses dynamic business objects that are inherited from a parent object type. For example the base class Client might have two sub classes called Vendor and ServiceProvider, and these are all handled by the same controller. I have a partial view that I load on the right side of the page when viewing the client's details called _Aside.cshtml. When I load the client I try to look for a specific Aside first and failing that I load a generic one. Below is what the code looks like.

@try
{
    @Html.Partial("_" + Model.Type.TypeName + "Aside")
}
catch (InvalidOperationException ex)
{
    @Html.Partial("_Aside")
}

The TypeName property would have the word "Vendor" or "ServiceProvider" in it.

Now this works fine but the problem is I only want it to fail over if the view is not found, It's also failing over when there is an actual InvalidOperationException thrown by the partial view (usually the result of a child action it might call). I've thought about checking against Exception.Message but that seems a bit hackish. Is there some other way I can get the desired result without having to check the Message property or is that my only option at this point?

ex.Message = "The partial view '_ServiceProviderAside' was not found or no view
              engine supports the searched locations. The following locations were
              searched: (... etc)"

UPDATE: This is the class with extension methods I have currently in my project based off of Jack's answer, and Chao's suggestions as well.

//For ASP.NET MVC
public static class ViewExtensionMethods
{
    public static bool PartialExists(this HtmlHelper helper, string viewName)
    {
        if (string.IsNullOrEmpty(viewName)) throw new ArgumentNullException(viewName, "View name cannot be empty");
        var view = ViewEngines.Engines.FindPartialView(helper.ViewContext, viewName);
        return view.View != null;
    }
    public static bool PartialExists(this ControllerContext controllerContext, string viewName)
    {
        if (string.IsNullOrEmpty(viewName)) throw new ArgumentNullException(viewName, "View name cannot be empty");
        var view = ViewEngines.Engines.FindPartialView(controllerContext, viewName);
        return view.View != null;
    }

    public static MvcHtmlString OptionalPartial(this HtmlHelper helper, string viewName)
    {
        return PartialExists(helper, viewName) ? helper.Partial(viewName) : HtmlString.Empty;
    }

    public static MvcHtmlString OptionalPartial(this HtmlHelper helper, string viewName, string fallbackViewName)
    {
        return OptionalPartial(helper, viewName, fallbackViewName, null);
    }

    public static MvcHtmlString OptionalPartial(this HtmlHelper helper, string viewName, object model)
    {
        return PartialExists(helper, viewName) ? helper.Partial(viewName, model) : MvcHtmlString.Empty;
    }

    public static MvcHtmlString OptionalPartial(this HtmlHelper helper, string viewName, string fallbackViewName, object model)
    {
        return helper.Partial(PartialExists(helper, viewName) ? viewName : fallbackViewName, model);
    }

    public static void RenderOptionalPartial(this HtmlHelper helper, string viewName)
    {
        if (PartialExists(helper, viewName))
        {
            helper.RenderPartial(viewName);
        }
    }

    public static void RenderOptionalPartial(this HtmlHelper helper, string viewName, string fallbackViewName)
    {
        helper.RenderPartial(PartialExists(helper, viewName) ? viewName : fallbackViewName);
    }
}

UPDATE: If you happen to be using ASP.NET Core MVC, swap the PartialExists() methods for these three methods, and change all of the usages of HtmlHelper for IHtmlHelper in the other methods. Skip this if you're not using ASP.NET Core

//For ASP.NET Core MVC
public static class ViewExtensionMethods
{
    public static bool PartialExists(this IHtmlHelper helper, string viewName)
    {
        var viewEngine = helper.ViewContext.HttpContext.RequestServices.GetService<ICompositeViewEngine>();
        if (string.IsNullOrEmpty(viewName)) throw new ArgumentNullException(viewName, "View name cannot be empty");
        var view = viewEngine.FindView(helper.ViewContext, viewName, false);
        return view.View != null;
    }

    public static bool PartialExists(this ControllerContext controllerContext, string viewName)
    {
        var viewEngine = controllerContext.HttpContext.RequestServices.GetService<ICompositeViewEngine>();
        if (string.IsNullOrEmpty(viewName)) throw new ArgumentNullException(viewName, "View name cannot be empty");
        var view = viewEngine.FindView(controllerContext, viewName, false);
        return view.View != null;
    }

    public static bool PartialExists(this ViewContext viewContext, string viewName)
    {
        var viewEngine = viewContext.HttpContext.RequestServices.GetService<ICompositeViewEngine>();
        if (string.IsNullOrEmpty(viewName)) throw new ArgumentNullException(viewName, "View name cannot be empty");
        var view = viewEngine.FindView(viewContext, viewName, false);
        return view.View != null;
    }
}

In my view...

@Html.OptionalPartial("_" + Model.Type.TypeName + "Aside", "_Aside")
//or
@Html.OptionalPartial("_" + Model.Type.TypeName + "Aside", "_Aside", Model.AsideViewModel)
Warmongering answered 22/2, 2013 at 16:40 Comment(0)
P
1

You could try the FindPartialView method to check if the view exists. Something along these lines might work (untested):

public bool DoesViewExist(string name)
 {
     string viewName = "_" + Model.Type.TypeName + "Aside";
     ViewEngineResult viewResult = ViewEngines.Engines.FindPartialView(ControllerContext, viewName , null);
     return (viewResult.View != null);
 }

Info on the FindPartialView method for ASP MVC 3

Provincetown answered 22/2, 2013 at 19:6 Comment(1)
That should work actually, I was too focused on handling the error I didn't think to look for the view using the exposed methods of the view engine. I've modified the question to include the code I came up with.Warmongering
C
5

Came across this answer while trying to solve the problem of nested sections as I wanted to include styles and scripts in an intermediate view. I ended up deciding the simplest approach was convention of templatename_scripts and templatename_styles.

So just to add to the various options here is what I'm using based on this.

public static class OptionalPartialExtensions
{
    public static MvcHtmlString OptionalPartial(this HtmlHelper helper, string viewName)
    {
        return PartialExists(helper, viewName) ? helper.Partial(viewName) : MvcHtmlString.Empty;
    }

    public static MvcHtmlString OptionalPartial(this HtmlHelper helper, string viewName, string fallbackViewName)
    {
        return helper.Partial(PartialExists(helper, viewName) ? viewName : fallbackViewName);
    }

    public static void RenderOptionalPartial(this HtmlHelper helper, string viewName)
    {
        if (PartialExists(helper, viewName))
        {
            helper.RenderPartial(viewName);
        }
    }

    public static void RenderOptionalPartial(this HtmlHelper helper, string viewName, string fallbackViewName)
    {
        helper.RenderPartial(PartialExists(helper, viewName) ? viewName : fallbackViewName);
    }

    public static bool PartialExists(this HtmlHelper helper, string viewName)
    {
        if (string.IsNullOrEmpty(viewName))
        {
            throw new ArgumentNullException(viewName, "View name cannot be empty");
        }
        var view = ViewEngines.Engines.FindPartialView(helper.ViewContext, viewName);
        return view.View != null;
    }
}

This brings the my most common use cases in to the extension methods helping keep the views that bit cleaner, the RenderPartials were added for completeness.

Charpentier answered 8/10, 2015 at 11:42 Comment(5)
Ahh! Good call. Thanks for sharing this. That would be the best way to keep the logic of how to fail over to a different view, out of the parent view. I've been working on getting more and more of my logic out of views and into controllers, or methods like this too. I think I'll swap my implementation to use this style as well, thanks :-)Warmongering
No problems, I've been making pushes to do the same myself, I shudder every time I open up some of my older views full of logic.Charpentier
Hahaha, yes I know exactly what you mean. I will mention though, in my case I had to refactor the methods to have an overloaded version letting me pass in a viewmodel different than that of the parent view (usually a child property of the parent viewmodel). I didn't want my partial views to depend on the same viewmodels as the parents', as they are typically single purpose models, my partial views' models are typically the generic ones across multiple views..Warmongering
I tried having a go at that myself to give a really nice set of overloads but started getting into a mess with too many branching possibilities (model just for the fallback, different models for the normal and the fallback, does one overload to null etc). I think it's the right approach but I think that level needs to be up to the requirements of the individual project.Charpentier
Ahh good call. In my case it was the same view models for both the optional view and the fallback one, so I could rely on that assumption for my overloads. For me, the ViewModel was the only parameter I needed to account for in my overloads, so yeah, it'd be very much up to the project needs.Warmongering
J
3

I had a similar requirement. I wanted to keep the view markup cleaner and also to avoid generating the dynamic view name twice. This is what I came up with (modified to match your example):

Helper extension:

public static string FindPartial(this HtmlHelper html, string typeName)
{
    // If you wanted to keep it in the view, you could move this concatenation out:
    string viewName = "_" + typeName + "Aside";

    ViewEngineResult result = ViewEngines.Engines.FindPartialView(html.ViewContext, viewName);
    if (result.View != null)
        return viewName;

    return "_Aside";
}

View:

@Html.Partial(Html.FindPartial(Model.Type.TypeName))

or with access to the Model within the partial :

@Html.Partial(Html.FindPartial(Model.Type.TypeName), Model)
Justiciable answered 1/10, 2014 at 8:28 Comment(0)
P
1

You could try the FindPartialView method to check if the view exists. Something along these lines might work (untested):

public bool DoesViewExist(string name)
 {
     string viewName = "_" + Model.Type.TypeName + "Aside";
     ViewEngineResult viewResult = ViewEngines.Engines.FindPartialView(ControllerContext, viewName , null);
     return (viewResult.View != null);
 }

Info on the FindPartialView method for ASP MVC 3

Provincetown answered 22/2, 2013 at 19:6 Comment(1)
That should work actually, I was too focused on handling the error I didn't think to look for the view using the exposed methods of the view engine. I've modified the question to include the code I came up with.Warmongering
S
0

Bug fix to handle null viewName or null fallbackViewName (replace appropriate code in OP):

public static MvcHtmlString OptionalPartial(this HtmlHelper helper, string viewName, string fallbackViewName, object model)
{
    string partialToRender = null;
    if (viewName != null && PartialExists(helper, viewName))
    {
        partialToRender = viewName;
    }
    else if (fallbackViewName != null && PartialExists(helper, fallbackViewName)) 
    {
        partialToRender = fallbackViewName;
    }
    if (partialToRender != null)
    {
        return helper.Partial(partialToRender, model);
    }
    else
    {
        return MvcHtmlString.Empty;
    }
}

I have edited the OP's code (which combines code from multiple answers), but my edit is pending peer review.

Sherwoodsherwynd answered 12/10, 2017 at 0:12 Comment(4)
This isn't really needed. The idea was to only render a view if it exists, and if not, render the fallback view. If the view requested is null, it obviously won't exist and it'll render the fallback. If the fallback is null (which should never happen) then the call to the IHtmlContent.Partial extension method will throw an error. What you're really doing is swallowing the error when this happens, while this might be what you're looking for, it throws an error for a reason, and I'd rather not suggest that others to do the same and let them make that decision.Warmongering
@NickAlbrecht I prevented the exception by allowing null to mean "render nothing". In my case, I don't need a fallback view, but the code was failing when calling OptionalPartial with just one view name. (Was this method untested?) I also added a check for the main view name because some people may dynamically generate the view name and null seems a better way to indicate no view than a fake name. My use case: I have an application template (installed via NuGet) and each application may (or may not) implement optional views which I don't want pre-installed (so not overwritten by updates).Sherwoodsherwynd
Ahh you're right, that overload was untested. I actually didn't have any usages of that overload in my project yet. I've updated the code in my post to reflect the corrections. Thanks for pointing this out! The point I was trying to make above is that the overload that accepts both an optional view name AND a fallback view name, there should never be a scenario where the fallback view is null.Warmongering
Your update should work for my scenario, which are placeholders in a LOB SPA template like this: @Html.OptionalPartial("app_body_end") and the actual app may or may not implement app_body_end.cshtml (most don't, but it serves as an extensibility point w/o having to edit a common index.cshtml installed and updated via NuGet).For clarity, another scenario that I thought of was something like @Html.OptionalPartial(GetElevatedCommandBarViewNameForUser()), and the view name may be "admin", "developer", or null (depending on user rights). Thanks for posting the question and maintaining it!Sherwoodsherwynd

© 2022 - 2024 — McMap. All rights reserved.