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
Short story about what I am doing and why. I had been doing view to string conversion in Mvc project, but suddenly all project moved to the REST API. But I had to use razor engine to convert my view with all model data there, so I was trying to use directly from api, but it didn't work for me, so I decided to create a Mvc controller and call it directly from API, what is missing, only ControllerContext, because when I create controller directly, it appears to be null.
Here is my Api controller
public class AttachmentController : ApiController
{
[HttpGet]
public async Task<IHttpActionResult> Get(long id)
{
try
{
var mvcController = new AttachmentMvcController();
var result = await mvcController.Get();
return Ok(result);
}
catch (Exception e)
{
return InternalServerError(e);
}
}
}
and this is my Mvc Controller
public class AttachmentMvcController : Controller
{
public AttachmentMvcController(){ }
public async Task<byte[]> Get()
{
string result;
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
// HERE IS MY PROBLEM, I NEED this.ControllerContext, but it is null !!!
if (ControllerContext == null)
{
// create it and do task
var factory = DependencyResolver.Current.GetService<IControllerFactory>() ?? new DefaultControllerFactory();
AttachmentMvcController controller = factory.CreateController(ctx.RequestContext, "AttachmentMvc") as AttachmentMvcController;
RouteData route = new RouteData();
route.Values.Add("action", "ActionThatUsesControllerContext"); // ActionName, but it not required
ControllerContext newContext = new ControllerContext(new HttpContextWrapper(System.Web.HttpContext.Current), route, controller);
controller.ControllerContext = newContext;
result = await controller.ActionThatUsesControllerContext(id);
}
else
{
result = await this.ActionThatUsesControllerContext(id);
}
return result;
}
private async Task<byte[]> ActionThatUsesControllerContext()
{
{....}
// here I am trying to use helper that uses that controller context
string viewAsString = ControllerContext.RenderPartialToString("MyView", requestModel);
{....}
}
}
If anyone has idea how to get that ControllerContext or any other ways to get my razor engine render view inside ApiController, please share.
I have some inbuilt method in BootstrapBaseController.cs like
public void Success(string message)
{
TempData.Add(Alerts.SUCCESS, message);
}
I haven't used it yet but I guess it shows some success message that i pass to this method.
I want to use this method in my other controller called PortalController. In following method I want to show success message when a user is added to database.
Suggestions?
[HttpPost]
public ActionResult Register(RegisterModel user)
{
if (ModelState.IsValid && (user.Password == user.ConfirmPassword))
{
var regUser = _db.Users.Create();
regUser.UserName = user.UserName;
regUser.Password = user.Password;
_db.Users.Add(regUser);
_db.SaveChanges();
}
return View();
}
You'll need to make PortalController inherits from BootstrapBaseController.
public class PortalController : BootstrapBaseController
If this is not an option you may need to move that common method to a third class used by both controllers.
Make PortalController inherit from BootstrapBaseController
Before returning the View call Sucess and set the message
On your _layout.cshtml view (and I'm assuming all your views inherit use this layout) check for that temp variable and display your message
Updated Controller method:
[HttpPost]
public ActionResult Register(RegisterModel user)
{
if (ModelState.IsValid && (user.Password == user.ConfirmPassword))
{
var regUser = _db.Users.Create();
regUser.UserName = user.UserName;
regUser.Password = user.Password;
_db.Users.Add(regUser);
_db.SaveChanges();
}
Success("User Created successfully");
return View();
}
_Layout.cshtml
#if (TempData["VariableName"] != null)
{
<div>#TempData["VariableName"]</div>
}
On POST , if validation failed and before sending back the ViewModel to the same View with Model State errors, do you rebuild ViewModel for all SelectLists, ReadOnly fields etc?
right now I have separate methods for Fill First Time(for GET Edit-Method) / Rebuild ViewModels from domain objects, what is the best practice so I can be DRY and also not have to change two methods any time I add a new readonly property to ViewModel?
My Solution: Followed this Pattern
Followed pattern suggested here: https://stackoverflow.com/a/2775656/57132
In IModelBuilder Implementation
Build(..)
{
var viewModel = new ViewModel();
// and Fill all Non-ReadOnly fields
...
...
call CompleteViewModel(viewModel)
}
CompleteViewModel(ViewModel viewModel)
{
//Fill all ReadOnly & SelectLists
...
}
The reason I went with this solution is because I don't want to store stuff on server to retrieve across the HTTP Requests
I don't rebuild it, because I don't stay at POST. I follow POST-REDIRECT-GET pattern, so if I post to /User/Edit/1 using POST HTTP method, I get redirected to /User/Edit/1 uasing GET.
ModelState is transferred to TempData to follow Post-Redirect-Get and be availabe at GET call. View model is built in one place, at GET call. Example:
[HttpPost]
[ExportModelStateToTempData]
public ActionResult Edit(int id, SomeVM postedModel)
{
if (ModelState.IsValid) {
//do something with postedModel and then go back to list
return RedirectToAction(ControllerActions.List);
}
//return back to edit, because there was an error
return RedirectToAction(ControllerActions.Edit, new { id });
}
[ImportModelStateFromTempData]
public ActionResult Edit(int id)
{
var model = //create model here
return View(ControllerActions.Edit, model);
}
This is code for attributes importing/exporting ModelState:
public abstract class ModelStateTempDataTransferAttribute : ActionFilterAttribute
{
protected static readonly string Key = typeof(ModelStateTempDataTransferAttribute).FullName;
}
public class ExportModelStateToTempDataAttribute : ModelStateTempDataTransferAttribute
{
public override void OnActionExecuted(ActionExecutedContext filterContext)
{
//Only export when ModelState is not valid
if (!filterContext.Controller.ViewData.ModelState.IsValid)
{
//Export if we are redirecting
if ((filterContext.Result is RedirectResult) || (filterContext.Result is RedirectToRouteResult))
{
filterContext.Controller.TempData[Key] = filterContext.Controller.ViewData.ModelState;
}
}
base.OnActionExecuted(filterContext);
}
}
public class ImportModelStateFromTempDataAttribute : ModelStateTempDataTransferAttribute
{
public override void OnActionExecuted(ActionExecutedContext filterContext)
{
ModelStateDictionary modelState = filterContext.Controller.TempData[Key] as ModelStateDictionary;
if (modelState != null)
{
//Only Import if we are viewing
if (filterContext.Result is ViewResult)
{
filterContext.Controller.ViewData.ModelState.Merge(modelState);
}
else
{
//Otherwise remove it.
filterContext.Controller.TempData.Remove(Key);
}
}
base.OnActionExecuted(filterContext);
}
}
The simplest solution would be to pass in you viewModel to the method and account for null
private MyViewModel BuildViewModel(MyViewModel model = null)
{
model = model ?? new MyViewModel();
model.ReadOnlyList = new .....
.
.
return model;
}
for Create:
var model = BuildViewModel();
for rebuild:
model = buildViewModel(model);
I like #LukLed's answer above - it looks very interesting. If you want another option, here's what I currently do.
In my service layer, I have a method to build my view model. I call that on GET and return the the view model to the view. On POST, I build the model from the incoming ID and then TryUpdateModel(model). From there, you can do whatever you like (save, check model state, etc.). With this method, you only have 1 build method and only have to update it once if your model changes (i.e. add/remove properties in the future, etc.).
[HttpGet]
public ActionResult AssessFocuses(int apaID)
{
var model = this.apaService.BuildAssessFocusesViewModel(apaID);
return this.View(model);
}
[HttpPost]
public ActionResult AssessFocuses(int apaID, string button)
{
var model = this.apaService.BuildAssessFocusesViewModel(apaID);
this.TryUpdateModel(model);
switch (button)
{
case ButtonSubmitValues.Back:
case ButtonSubmitValues.Next:
case ButtonSubmitValues.Save:
case ButtonSubmitValues.SaveAndClose:
{
try
{
this.apaService.SaveFocusResults(model);
}
catch (ModelStateException<AssessFocusesViewModel> mse)
{
mse.ApplyTo(this.ModelState);
}
if (!this.ModelState.IsValid)
{
this.ShowErrorMessage(Resources.ErrorMsg_WEB_ValidationSummaryTitle);
return this.View(model);
}
break;
}
default:
throw new InvalidOperationException(string.Format(Resources.ErrorMsg_WEB_InvalidButton, button));
}
switch (button)
{
case ButtonSubmitValues.Back:
return this.RedirectToActionFor<APAController>(c => c.EnterRecommendationsPartner(model.ApaID));
case ButtonSubmitValues.Next:
return this.RedirectToActionFor<APAController>(c => c.AssessCompetenciesPartner(model.ApaID));
case ButtonSubmitValues.Save:
this.ShowSuccessMessage(Resources.Msg_WEB_NotifyBarSuccessGeneral);
return this.RedirectToActionFor<APAController>(c => c.AssessFocuses(model.ApaID));
case ButtonSubmitValues.SaveAndClose:
default:
return this.RedirectToActionFor<UtilityController>(c => c.CloseWindow());
}
}
I am developing MVC application.
I want to pass controller to some other class for validation purpose.
After passing the controller, I am unable to get the controller name in that class.
[HttpPost]
public ActionResult Create(Location location)
{
if (ModelState.IsValid)
{
Validations v = new Validations();
boolean b;
//passing controller in another class's method
b = v.ValidProperty(location);
if (ValidProperties == true)
{
db.Locations.Add(location);
db.SaveChanges();
return RedirectToAction("Index");
}
}
}
Getting controller in below method
public void ValidProperty(object Controller)
{
//Gives an error in below line
string CtrName = (string)Controller.ToString;
}
How to get the controller Name ?
b = v.ValidProperty(ControllerContext);
you may be wondering where am I initializing ControllerContext variable.
well you don't have to
public void ValidProperty(ControllerContext ControllerContext)
{
// do your logic here.
}
You should call ControllerContext.RouteContext.GetRequiredString("controller")
To get the name of the controller, you can just use
RouteData.Values["controller"]
Anyone understand why the following doesn't work?
What I want to do is copy current route data plus whatever I add via an anonymous object into new routedata when forming new links on the view.
For example if I have the parameter "page" as a non route path (i.e. so it overflows the route path and its injected into the method parameter if a querystring is present) e.g.
public ActionResult ChangePage(int? page) { }
and I want the View to know the updated page when building links using helpers. I thought the best way to do this is with the following:
public ActionResult ChangePage(int? page)
{
if(page.HasValue)
RouteData.Values.Add("Page", page);
ViewData.Model = GetData(page.HasValue ? page.Value : 1);
}
Then in the view markup I can render my next, preview, sort, showmore (any links relevant) with this overload:
public static class Helpers
{
public static string ActionLinkFromRouteData(this HtmlHelper helper, string linkText, string actionName, object values)
{
RouteValueDictionary routeValueDictionary = new RouteValueDictionary();
foreach(var routeValue in helper.ViewContext.RouteData.Values)
{
if(routeValue.Key != "controller" && routeValue.Key != "action")
{
routeValueDictionary[routeValue.Key] = routeValue;
}
}
foreach(var prop in GetProperties(values))
{
routeValueDictionary[prop.Name] = prop.Value;
}
return helper.ActionLink(linkText, actionName, routeValueDictionary;
}
private static IEnumerable<PropertyValue> GetProperties(object o)
{
if (o != null) {
PropertyDescriptorCollection props = TypeDescriptor.GetProperties(o);
foreach (PropertyDescriptor prop in props) {
object val = prop.GetValue(o);
if (val != null) {
yield return new PropertyValue { Name = prop.Name, Value = val };
}
}
}
}
private sealed class PropertyValue
{
public string Name { get; set; }
public object Value { get; set; }
}
}
I have posted the code only to illustrate the point. This doesn't work and doesn't feel right... Pointers?
Pass the page info into ViewData?
PagedResultsInfo (or something) sounds like a class you could write too... we do.