ASP.NET MVC 3 - render an optional partial view [duplicate] - c#

Is it possible to determine if a specific view name exists from within a controller before rendering the view?
I have a requirement to dynamically determine the name of the view to render. If a view exists with that name then I need to render that view. If there is no view by the custom name then I need to render a default view.
I'd like to do something similar to the following code within my controller:
public ActionResult Index()
{
var name = SomeMethodToGetViewName();
// The 'ViewExists' method is what I've been unable to find.
if (ViewExists(name))
{
retun View(name);
}
else
{
return View();
}
}

private bool ViewExists(string name)
{
ViewEngineResult result = ViewEngines.Engines.FindView(ControllerContext, name, null);
return (result.View != null);
}
For those looking for a copy/paste extension method:
public static class ControllerExtensions
{
public static bool ViewExists(this Controller controller, string name)
{
ViewEngineResult result = ViewEngines.Engines.FindView(controller.ControllerContext, name, null);
return (result.View != null);
}
}

What about trying something like the following assuming you are using only one view engine:
bool viewExists = ViewEngines.Engines[0].FindView(ControllerContext, "ViewName", "MasterName", false) != null;

Here's another [not necessarily recommended] way of doing it
try
{
#Html.Partial("Category/SearchPanel/" + Model.CategoryKey)
}
catch (InvalidOperationException) { }

In asp.net core 2.x and aspnet6 the ViewEngines property no longer exists so we have to use the ICompositeViewEngine service. This a variant of the accepted answer using dependency injection:
public class DemoController : Controller
{
private readonly IViewEngine _viewEngine;
public DemoController(ICompositeViewEngine viewEngine)
{
_viewEngine = viewEngine;
}
private bool ViewExists(string name)
{
ViewEngineResult viewEngineResult = _viewEngine.FindView(ControllerContext, name, true);
return viewEngineResult?.View != null;
}
public ActionResult Index() ...
}
For the curious: The base interface IViewEngine is not registered as a service so we must inject ICompositeViewEngine instead. The FindView() method however is provided by IViewEngine so the member variable may use the base interface.

If you want to re-use this across multiple controller actions, building on the solution given by Dave, you can define a custom view result as follows:
public class CustomViewResult : ViewResult
{
protected override ViewEngineResult FindView(ControllerContext context)
{
string name = SomeMethodToGetViewName();
ViewEngineResult result = ViewEngines.Engines.FindView(context, name, null);
if (result.View != null)
{
return result;
}
return base.FindView(context);
}
...
}
Then in your action simply return an instance of your custom view:
public ActionResult Index()
{
return new CustomViewResult();
}

ViewEngines.Engines.FindView(ViewContext.Controller.ControllerContext, "View Name").View != null
My 2 cents.

Here's how to do it in Razor for Core 2.2 etc. Note that the call is "GetView", not "Find View)
#using Microsoft.AspNetCore.Mvc.ViewEngines
#inject ICompositeViewEngine Engine
...
#if (Engine.GetView(scriptName, scriptName, isMainPage: false).Success)
{
#await Html.PartialAsync(scriptName)
}

Related

Can I use Content Negotiation to return a View to browers and JSON to API calls in ASP.NET Core?

I've got a pretty basic controller method that returns a list of Customers. I want it to return the List View when a user browses to it, and return JSON to requests that have application/json in the Accept header.
Is that possible in ASP.NET Core MVC 1.0?
I've tried this:
[HttpGet("")]
public async Task<IActionResult> List(int page = 1, int count = 20)
{
var customers = await _customerService.GetCustomers(page, count);
return Ok(customers.Select(c => new { c.Id, c.Name }));
}
But that returns JSON by default, even if it's not in the Accept list. If I hit "/customers" in my browser, I get the JSON output, not my view.
I thought I might need to write an OutputFormatter that handled text/html, but I can't figure out how I can call the View() method from an OutputFormatter, since those methods are on Controller, and I'd need to know the name of the View I wanted to render.
Is there a method or property I can call to check if MVC will be able to find an OutputFormatter to render? Something like the following:
[HttpGet("")]
public async Task<IActionResult> List(int page = 1, int count = 20)
{
var customers = await _customerService.GetCustomers(page, count);
if(Response.WillUseContentNegotiation)
{
return Ok(customers.Select(c => new { c.Id, c.Name }));
}
else
{
return View(customers.Select(c => new { c.Id, c.Name }));
}
}
I think this is a reasonable use case as it would simplify creating APIs that return both HTML and JSON/XML/etc from a single controller. This would allow for progressive enhancement, as well as several other benefits, though it might not work well in cases where the API and Mvc behavior needs to be drastically different.
I have done this with a custom filter, with some caveats below:
public class ViewIfAcceptHtmlAttribute : Attribute, IActionFilter
{
public void OnActionExecuted(ActionExecutedContext context)
{
if (context.HttpContext.Request.Headers["Accept"].ToString().Contains("text/html"))
{
var originalResult = context.Result as ObjectResult;
var controller = context.Controller as Controller;
if(originalResult != null && controller != null)
{
var model = originalResult.Value;
var newResult = controller.View(model);
newResult.StatusCode = originalResult.StatusCode;
context.Result = newResult;
}
}
}
public void OnActionExecuting(ActionExecutingContext context)
{
}
}
which can be added to a controller or action:
[ViewIfAcceptHtml]
[Route("/foo/")]
public IActionResult Get(){
return Ok(new Foo());
}
or registered globally in Startup.cs
services.AddMvc(x=>
{
x.Filters.Add(new ViewIfAcceptHtmlAttribute());
});
This works for my use case and accomplishes the goal of supporting text/html and application/json from the same controller. I suspect isn't the "best" approach as it side-steps the custom formatters. Ideally (in my mind), this code would just be another Formatter like Xml and Json, but that outputs Html using the View rendering engine. That interface is a little more involved, though, and this was the simplest thing that works for now.
I haven't tried this, but could you just test for that content type in the request and return accordingly:
var result = customers.Select(c => new { c.Id, c.Name });
if (Request.Headers["Accept"].Contains("application/json"))
return Json(result);
else
return View(result);
I liked Daniel's idea and felt inspired, so here's a convention based approach as well. Because often the ViewModel needs to include a little bit more 'stuff' than just the raw data returned from the API, and it also might need to check different stuff before it does its work, this will allow for that and help in following a ViewModel for every View principal. Using this convention, you can write two controller methods <Action> and <Action>View both of which will map to the same route. The constraint applied will choose <Action>View if "text/html" is in the Accept header.
public class ContentNegotiationConvention : IActionModelConvention
{
public void Apply(ActionModel action)
{
if (action.ActionName.ToLower().EndsWith("view"))
{
//Make it match to the action of the same name without 'view', exa: IndexView => Index
action.ActionName = action.ActionName.Substring(0, action.ActionName.Length - 4);
foreach (var selector in action.Selectors)
//Add a constraint which will choose this action over the API action when the content type is apprpriate
selector.ActionConstraints.Add(new TextHtmlContentTypeActionConstraint());
}
}
}
public class TextHtmlContentTypeActionConstraint : ContentTypeActionConstraint
{
public TextHtmlContentTypeActionConstraint() : base("text/html") { }
}
public class ContentTypeActionConstraint : IActionConstraint, IActionConstraintMetadata
{
string _contentType;
public ContentTypeActionConstraint(string contentType)
{
_contentType = contentType;
}
public int Order => -10;
public bool Accept(ActionConstraintContext context) =>
context.RouteContext.HttpContext.Request.Headers["Accept"].ToString().Contains(_contentType);
}
which is added in startup here:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc(o => { o.Conventions.Add(new ContentNegotiationConvention()); });
}
In you controller, you can write method pairs like:
public class HomeController : Controller
{
public ObjectResult Index()
{
//General checks
return Ok(new IndexDataModel() { Property = "Data" });
}
public ViewResult IndexView()
{
//View specific checks
return View(new IndexViewModel(Index()));
}
}
Where I've created ViewModel classes meant to take the output of API actions, another pattern which connects the API to the View output and reinforces the intent that these two represent the same action:
public class IndexViewModel : ViewModelBase
{
public string ViewOnlyProperty { get; set; }
public string ExposedDataModelProperty { get; set; }
public IndexViewModel(IndexDataModel model) : base(model)
{
ExposedDataModelProperty = model?.Property;
ViewOnlyProperty = ExposedDataModelProperty + " for a View";
}
public IndexViewModel(ObjectResult apiResult) : this(apiResult.Value as IndexDataModel) { }
}
public class ViewModelBase
{
protected ApiModelBase _model;
public ViewModelBase(ApiModelBase model)
{
_model = model;
}
}
public class ApiModelBase { }
public class IndexDataModel : ApiModelBase
{
public string Property { get; internal set; }
}

Async Controller Action with Umbraco 7 returns string

Is it possible to use an async action within an Umbraco SurfaceController (and UmbracoApiController)
I tried the following code
public async Task< ActionResult> HandleLogin(LoginViewModel model)
{
await Task.Delay(1000);
return PartialView("Login", model);
}
and although it compiled correctly when the action is called the action seems to return as soon as the await is hit, and returns a string
System.Threading.Tasks.Task`1[System.Web.Mvc.ActionResult]
the controller of course inherits from SurfaceController and I wonder if this is the problem?
If this is not possible, are there any workarounds to achieve async action behaviour?
Any help would be gratefully received!
The SurfaceControllers in Umbraco ultimately derive from System.Web.Mvc.Controller However they have custom action invoker (RenderActionInvoker) set.
RenderActionInvoker inherits from ContollerActionInvoker. In order to process async actions it should instead derive from AsyncContolkerActionInvoker. RenderActionInvoker overrides only the findaction method so changing to derive from AsyncContolkerActionInvoker is easy.
Once I recompiled Umbraco.Web with this change, async actions worked fine.
Rather than recompiling the whole project, I guess you could specify a new actioninvoker on each class
public class RenderActionInvokerAsync : System.Web.Mvc.Async.AsyncControllerActionInvoker
{
protected override ActionDescriptor FindAction(ControllerContext controllerContext, ControllerDescriptor controllerDescriptor, string actionName)
{
var ad = base.FindAction(controllerContext, controllerDescriptor, actionName);
if (ad == null)
{
//check if the controller is an instance of IRenderMvcController
if (controllerContext.Controller is IRenderMvcController)
{
return new ReflectedActionDescriptor(
controllerContext.Controller.GetType().GetMethods()
.First(x => x.Name == "Index" &&
x.GetCustomAttributes(typeof(NonActionAttribute), false).Any() == false),
"Index",
controllerDescriptor);
}
}
return ad;
}
}
public class TestController : SurfaceController
{
public TestController() {
this.ActionInvoker = new RenderActionInvokerAsync();
}
public async Task<ActionResult> Test()
{
await Task.Delay(10000);
return PartialView("TestPartial");
}
}
Haven't tested this way of doing things though.
Just FYI I've added an issue to the tracker for this:
http://issues.umbraco.org/issue/U4-5208
There is a work around though:
Create a custom async render action invoke (as per above):
public class FixedAsyncRenderActionInvoker : System.Web.Mvc.Async.AsyncControllerActionInvoker
{
protected override ActionDescriptor FindAction(ControllerContext controllerContext, ControllerDescriptor controllerDescriptor, string actionName)
{
var ad = base.FindAction(controllerContext, controllerDescriptor, actionName);
if (ad == null)
{
//check if the controller is an instance of IRenderMvcController
if (controllerContext.Controller is IRenderMvcController)
{
return new ReflectedActionDescriptor(
controllerContext.Controller.GetType().GetMethods()
.First(x => x.Name == "Index" &&
x.GetCustomAttributes(typeof(NonActionAttribute), false).Any() == false),
"Index",
controllerDescriptor);
}
}
return ad;
}
}
Create a custom render mvc controller:
public class FixedAsyncRenderMvcController : RenderMvcController
{
public FixedAsyncRenderMvcController()
{
this.ActionInvoker = new FixedAsyncRenderActionInvoker();
}
}
Create a custom render controller factory:
public class FixedAsyncRenderControllerFactory : RenderControllerFactory
{
public override IController CreateController(RequestContext requestContext, string controllerName)
{
var controller1 = base.CreateController(requestContext, controllerName);
var controller2 = controller1 as Controller;
if (controller2 != null)
controller2.ActionInvoker = new FixedAsyncRenderActionInvoker();
return controller1;
}
}
Create an umbraco startup handler and replace the necessary parts with the above custom parts:
public class UmbracoStartupHandler : ApplicationEventHandler
{
protected override void ApplicationStarting(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext)
{
DefaultRenderMvcControllerResolver.Current.SetDefaultControllerType(typeof(FixedAsyncRenderMvcController));
FilteredControllerFactoriesResolver.Current.RemoveType<RenderControllerFactory>();
FilteredControllerFactoriesResolver.Current.AddType<FixedAsyncRenderControllerFactory>();
base.ApplicationStarting(umbracoApplication, applicationContext);
}
}

How to call controller action from different project which don't return view

I have 2 projects MVCMembership and main web project which using membership and i need to get some date in controller which is in membership project from controller in my main web project.This is what i try:
this is from my membership controller
public ViewResult Index(int? index)
{
if (Roles.IsUserInRole("Group Admin"))
{
string[] roles = Roles.GetRolesForUser();
var GroupUsers = RedirectToAction("UsersInGroup", "Account", new { Area = "" });
and this is action in my web project controller:
public MembershipUserCollection UsersInGroup()
{
var groupResultSet = db.aspnet_UsersInGroups.Where(u => u.UserID == (Guid)Membership.GetUser().ProviderUserKey);
var group = groupResultSet.Single().aspnet_Group;
return group.Users;
}
So this is not working because RedirectToAction returns RedirectToRouteResult.
Is there some way to do this?
To be more clear imagine you have 2 simple methods in one class
public somthing Method1()
{
//doing something
//call some other method
var parm = Method2();
//doing something whit parm
return somethingelse;
}
public something Method2()
{
//doing something
return parm;
}
and now put that in context from up.Is that possible to do it?
There are two ways of performing this.
The Bad:
Actions in a controller are simply methods so you can call them directly you would need to create and instance of the controller and then call your method:
public ViewResult Index(int? index)
{
if (Roles.IsUserInRole("Group Admin"))
{
string[] roles = Roles.GetRolesForUser();
var accountController = new AccountController()
var GroupUsers = accountController.UsersInGroup();
The Good: Read up on wiring up a IOC container into your MVC application, remove the method you require from the controller and place it into a service and inject the service into your controller.
public YourController:Controller
{
private IMembershipService _membership
public YourController(IMembershipService membership)
{
_membership = membership;
}
public ViewResult Index(int? index)
{
if (Roles.IsUserInRole("Group Admin"))
{
string[] roles = Roles.GetRolesForUser();
var GroupUsers = _membership.UsersInGroup();
If you can call controller from other project you can just use Response.Redirect(
public EmptyResult Home()
{
Response.Redirect("http://YourDoimein.com/Controller/Action");
}

Posting data when my view model has a constructor does not work

I have the following code:
[HttpGet]
public ActionResult Edit(int req)
{
var viewModel = new EditViewModel();
viewModel.RequestId = int;
return View(viewModel);
}
[HttpPost]
Public ActionResult Edit(EditViewModel viewModel)
{
// some code here...
}
It works fine: when the edit form is posted, I have the action controller who is called.
Now I modify some little bit my code like this:
[HttpGet]
public ActionResult Edit(int req)
{
var viewModel = new EditViewModel(req);
return View(viewModel);
}
[HttpPost]
Public ActionResult Edit(EditViewModel viewModel)
{
// some code here...
}
public class EditViewModel()
{
public EditViewModel(int req)
{
requestId = req;
}
...
}
In this new version, I have a view model with a contructor.
This time, when my form is posted back, the action controller is never triggered.
Any idea?
Thanks.
That's normal. The default model binder can no longer instantiate your view model as it doesn't have a parameterless constructor. You will have to write a custom model binder if you want to use view models that don't have a default constructor.
Normally you don't need such custom constructor. You could simply have your view model like that:
public class EditViewModel()
{
public int RequestId { get; set; }
}
and the POST action like that:
[HttpPost]
public ActionResult Edit(EditViewModel viewModel)
{
// some code here...
}
and now all you have to do is POST the requestId parameter instead of req and the default model binder will do the job.
And if for some reason you wanted to use a view model with custom constructor, here's an example of how the custom model binder might look like:
public class EditViewModelBinder : DefaultModelBinder
{
protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType)
{
var req = bindingContext.ValueProvider.GetValue("req");
if (req == null)
{
throw new Exception("missing req parameter");
}
int reqValue;
if (!int.TryParse(req.AttemptedValue, out reqValue))
{
throw new Exception(string.Format("The req parameter contains an invalid value: {0}", req.AttemptedValue));
}
return new EditViewModel(reqValue);
}
}
which will be registered in your Application_Start:
ModelBinders.Binders.Add(typeof(EditViewModel), new EditViewModelBinder());
Asp.Net Core version
public class EditViewModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null)
{
throw new ArgumentNullException(nameof(bindingContext));
}
var req = bindingContext.ValueProvider.GetValue("req");
if (req == ValueProviderResult.None || string.IsNullOrEmpty(req.FirstValue))
{
bindingContext.ModelState.TryAddModelError("req", "Missing req parameter");
}
int reqValue;
if (!int.TryParse(req.AttemptedValue, out reqValue))
{
bindingContext.ModelState.TryAddModelError($"The req parameter contains an invalid value: {req.AttemptedValue}");
}
var model = new EditViewModel(req.FirstValue);
bindingContext.Result = ModelBindingResult.Success(model);
return Task.CompletedTask;
}
}
You don't need to register anything with startup.cs anymore. Just assign the binder to your ViewModel and you're away.
[ModelBinder(BinderType = typeof(EditViewModelBinder))]
public class EditViewModel

ASP.NET MVC - HybridViewResult (ViewResult /PartialViewResult)

Is it possible to build a hybrid ViewResult that returns in depedency of an AjaxRequest or HttpRequest a PartialViewResult or ViewResult?
IsAjaxRequest --> return PartialViewResult
!IsAjaxRequest --> return ViewResult
As far as I know my HybridViewResult should derive from ViewResultBase.
But how to implement the FindView method?
Try:
public class HybridViewResult : ActionResult
{
public string ViewName { get; set; }
public HybridViewResult () { }
public HybridViewResult (string viewName ) { this.ViewName = viewName ; }
public override void ExecuteResult(ControllerContext context)
{
if (context == null) throw new ArgumentNullException("context");
var usePartial = ShouldUsePartial();
ActionResult res = GetInnerViewResult(usePartial);
res.ExecuteResult(context);
}
private ActionResult GetInnerViewResult(bool usePartial)
{
var view = ViewName;
ActionResult res;
if(String.IsNullOrEmpty(view)) {
res = usePartial ? new PartialViewResult(view) : new ViewResult(view);
}
else {
res = usePartial ? new PartialViewResult() : new ViewResult();
}
return res;
}
private bool ShouldUsePartial(ControllerContext context) {
return false; //your code that checks if you need to use partial here
}
}
Add any constructor & GetInnerViewResult variations as needed i.e. to pass Model.
This is a slightly more stripped down take on eglasius's answer. I'm actually tackling a similar problem except I need to return a JsonResult.
The (untested) NormalOrAjaxResult simply lets you specify an action result for the non ajax request and one for the ajax request. Because these are ActionResults you can mix up Redirect, View, Partial and Json view results.
public class NormalOrAjaxResult : ActionResult
{
private readonly ActionResult _nonAjaxActionResult;
private readonly ActionResult _ajaxActionResult;
public NormalOrAjaxResult(ActionResult nonAjaxActionResult, ActionResult ajaxActionResult)
{
_nonAjaxActionResult = nonAjaxActionResult;
_ajaxActionResult = ajaxActionResult;
}
public override void ExecuteResult(ControllerContext context)
{
var isAjaxRequest = context.HttpContext.Request["isAjax"];
if (isAjaxRequest != null && isAjaxRequest.ToLower() == "true")
{
_ajaxActionResult.ExecuteResult(context);
} else
{
_nonAjaxActionResult.ExecuteResult(context);
}
}
}
can you not just make 2 different actions in that case? the 'shared' logic you could simply put in a [nonAction] method?
I know I'm really late to the party here, but these didnt seem quite right to me, so here's my 2 cents:
public class PartialViewConverter : ViewResult
{
public ViewResultBase Res { get; set; }
public PartialViewConverter(ViewResultBase res) { Res = res; }
public override void ExecuteResult(ControllerContext context)
{
Res.ExecuteResult(context);
}
public static ViewResult Convert(ViewResultBase res)
{
return new PartialViewConverter(res);
}
}
With usage:
return PartialViewConverter.Convert(PartialView());
And then in your controller if you override View
protected override ViewResult View(string viewName, string masterName, object model)
{
//Whichever condition you like can go here
if (Request.QueryString["partial"] != null)
return PartialViewConverter.Convert(PartialView(viewName, model));
else
return base.View(viewName, masterName, model);
}
Any action method where you return a view will automatically also return partials when requested:
public ActionResult Index()
{
ViewBag.Message = "Modify this template to jump-start your ASP.NET MVC application.";
//This will return a partial if partial=true is passed in the querystring.
return View();
}

Categories