Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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)
like image 224
Nick Albrecht Avatar asked Feb 22 '13 16:02

Nick Albrecht


2 Answers

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.

like image 61
Chao Avatar answered Oct 10 '22 22:10

Chao


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)
like image 27
Ishmaeel Avatar answered Oct 10 '22 23:10

Ishmaeel