asp.net-mvcrazorpartial-viewsviewengine

Failover to an alternate View when Partial View is not found?


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)

Solution

  • 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