Dynamic RazorViewEngine for relating controllers dedicated to partials - c#

I'm working on what should be a minor RazorViewEngine Extension, but having some difficulty with the second part of my approach.
Basically I have some controllers that are dedicated to some partial views, and would like to be able to place them in sub directories. Of course placing the models and controllers in sub directories is easy enough, but the views need to match as well. That's where it gets tricky.
Below you'll see my engine, and what I'm missing is how to get the name of the master controller. It seems like maybe I should be able to extend ControllerContext somehow?
In the FileExists override you can see I hard coded in a value to test, and that much works. But I don't just want administration. An example of the controller relationship - I have an administration controller, which loads a few partials, each with their own dedicated controller, for example User. So when I'm loading views for User, I'd like it to go to Views/Administration/User instead of Views/User.
public class PartialsViewEngine : RazorViewEngine
{
private static string[] NewPartialViewFormats = new[] {
"~/Views/%1/{1}/{0}.cshtml",
"~/Views/{1}/Partials/{0}.cshtml",
"~/Views/Shared/Partials/{0}.cshtml"
};
public PartialsViewEngine()
{
base.PartialViewLocationFormats = base.PartialViewLocationFormats.Union(NewPartialViewFormats).ToArray();
}
protected override IView CreatePartialView(ControllerContext controllerContext, string partialPath)
{
string firstController = '????. //Trying to figure how to get this
var path = partialPath.Replace("%1", firstcontroller);
return base.CreatePartialView(controllerContext, path));
}
protected override bool FileExists(ControllerContext controllerContext, string virtualPath)
{
var path = partialPath.Replace("%1", "Administration");
return base.FileExists(controllerContext, path);
}
}
How would I, with the least overhead possible to the controllers, obtain that value?
--- Update 1 ---
I noticed with this configuration resharper doesn't recognize the file paths even though it executes.

Related

Where is the ViewResult returned by ASP.NET Core Controller consumed?

In the definition of the ASP.NET Core Controller class, it defines the View() method as returning a ViewResult object.
namespace Microsoft.AspNetCore.Mvc
{
//...
public abstract class Controller : ControllerBase, IActionFilter, IFilterMetadata, IAsyncActionFilter, IDisposable
{
//...
public virtual ViewResult View();
//...
}
}
Where is it in the framework that invokes a controller method such as below and consumes the ViewResult returned by the call to the View() method?
public class HomeController : Controller
{
public IActionResult Index()
{
return View();
}
}
The entire process is quite involved, and is managed internally by the ResourceInvoker class, but the main piece of plumbing responsible for handling a ViewResult is the ViewExecutor class (source code).
The primary entry point of the ViewExecutor is the ExecuteAsync() method, which accepts the ActionContext and your ViewResult. It then locates the appropriate view using the registered IViewEngine (typically the RazorViewEngine) to identify the corresponding view and return a ViewEngineResult instance. That ViewEngineResult is then used to execute and render the view.
Extending ViewExecutor
If you need to customize the logic, you can implement your own view executor class by implementing the IActionResultExecutor<> interface and then registering it as a singleton using ASP.NET Core's dependency injection container. When doing so, I recommend referencing the out-of-the-box ViewResultExecutor's source code.
This can be useful if you want to implement custom logic for locating the view. For instance, perhaps you want to account for state or context data such as route data, request headers, cookies, session, &c. when locating the view.
So, as a really simple example, let us say you want to allow a view to be optionally selected using a query string value (e.g., ?View=MyView). In that case, you might create a QueryStringViewResultExecutor. Here's a basic proof-of-concept:
public class QueryStringViewResultExecutor : ViewExecutor, IActionResultExecutor<ViewResult>
{
public QueryStringViewResultExecutor(
IOptions<MvcViewOptions> viewOptions,
IHttpResponseStreamWriterFactory writerFactory,
ICompositeViewEngine viewEngine,
ITempDataDictionaryFactory tempDataFactory,
DiagnosticListener diagnosticListener,
IModelMetadataProvider modelMetadataProvider
) : base(
viewOptions, writerFactory, viewEngine, tempDataFactory, diagnosticListener, modelMetadataProvider
)
{
}
public async Task ExecuteAsync(ActionContext context, ViewResult result)
{
var viewEngineResult = FindView(context, result); // See helper method below
viewEngineResult.EnsureSuccessful(originalLocations: null);
var view = viewEngineResult.View;
using (view as IDisposable)
{
await ExecuteAsync(
context,
view,
result.ViewData,
result.TempData,
result.ViewName,
result.StatusCode
).ConfigureAwait(false);
}
}
private ViewEngineResult FindView(ActionContext actionContext, ViewResult viewResult)
{
// Define variables
var view = (ViewEngineResult?)null;
var viewEngine = viewResult.ViewEngine?? ViewEngine;
var searchedPaths = new List<string>();
var requestContext = actionContext.HttpContext.Request;
// If defined, attempt to locate view based on query string variable
if (requestContext.Query.ContainsKey("View"))
{
var queryStringValue = requestContext.Query["View"].First<string>();
if (queryStringValue is not null)
{
view = viewEngine.FindView(actionContext, queryStringValue, isMainPage: true);
searchedPaths = searchedPaths.Union(view.SearchedLocations?? Array.Empty<string>()).ToList();
}
}
// If no view is found, fall back to the view defined on the viewResult
if (!view?.Success?? true)
{
view = viewEngine.FindView(actionContext, viewResult.ViewName, isMainPage: true);
searchedPaths = searchedPaths.Union(view.SearchedLocations ?? Array.Empty<string>()).ToList();
}
// Return view for processing by the razor engine
if (view is not null and { Success: true }) {
return view;
}
return ViewEngineResult.NotFound(viewResult.ViewName, searchedPaths);
}
}
You would then register this in your Startup.ConfigureServices() method as:
services.Services.TryAddSingleton<IActionResultExecutor<ViewResult>, QueryStringViewResultExecutor>();
Disclaimer: You may need to unregister the out-of-the-box ViewExecutor in order to avoid a conflict. Typically, however, you are registering an IActionResultExecutor<> with a custom ViewResult and, thus, that isn't necessary; see below.
Extending ViewResult
Often, you will want to pair this with a custom ViewResult. Why is this useful? Usually because you need to pass additional data to your ViewResultExecutor from your Controller.
So, as a contrived example, imagine that you have themes, and views can optionally be customized based on that theme. You might then add a Theme property to your ViewResult, thus allowing the ViewResultExecutor to first look for a view based on the theme, and otherwise fallback to the non-themed version.
public class ThemedViewResult : ViewResult
{
public string Theme { get; set; }
public override async Task ExecuteResultAsync(ActionContext context)
{
var executor = context.HttpContext.RequestServices.GetRequiredService<IActionResultExecutor<ThemedViewResult>>();
await executor.ExecuteAsync(context, this).ConfigureAwait(false);
}
}
Even if you don't need a custom ViewResult, it's worth pausing for a second to evaluate this code. The underlying ViewResult class implements IActionResult, which exposes a single method, ExecuteResultAsync(). This is called by the ResourceInvoker class mentioned at the top. This in turn locates the registered IActionResultExecutor<>—i.e., the type of component we created and registered in the previous section—and calls its ExecuteAsync() method.
Note: This is a contrived example because often a theme would be accessible by other context data, such as the RouteData, a custom ClaimsPrincipal, or an ISession implementation. But you can imagine other times where this information would be request-specific, and best relayed from the Controller via the ViewResult. For instance, perhaps a CMS where the theme can be selected on a per page basis.
As it's unclear whether you actually need to extend this functionality or are just curious how everything fits together, the above examples are only meant as a proof-of-concept; they aren't tested. Still, they should give you a basic idea of where and how the ViewResult is handled, as well as options for extending that behavior should you need.

does Razor view engine create IView object after parsing .cshtml?

My textbook shows an example of how to write custom view engine(implements IViewEngine, and the view it uses is a class that implements IView
public interface IViewEngine{
ViewEngineResult FindView(ActionContext context, string viewName, bool isMainPage);
ViewEngineResult GetView(string executingFilePath, string viewPath, bool isMainPage);
}
and
public interface IView {
string Path { get; }
Task RenderAsync(ViewContext context);
}
and below is a custom view that implements IView
public class DebugDataView : IView
{
public string Path => String.Empty;
public async Task RenderAsync(ViewContext context)
{
context.HttpContext.Response.ContentType = "text/plain";
StringBuilder sb = new StringBuilder();
sb.AppendLine("---Routing Data---");
foreach (var kvp in context.RouteData.Values)
{
sb.AppendLine($"Key: {kvp.Key}, Value: {kvp.Value}");
}
sb.AppendLine("---View Data---");
foreach (var kvp in context.ViewData)
{
sb.AppendLine($"Key: {kvp.Key}, Value: {kvp.Value}");
}
sb.AppendLine(context.ViewData.Model.ToString());
await context.Writer.WriteAsync(sb.ToString());
}
}
I have some questions
Q1-does mean that after the content in .cshtml view files is parsed by razor, then razor will create a View Object that implements IView behind the scene so that real content is implemented in RenderAsync?
Q2-the FindView or GetView method returns a ViewEngineResult object, then who will be turning ViewEngineResult object into response(html for clients), MVC or Razor Engine?
Not in the way that you'd think. There's technically an underlying View<TModel> (which implements IView), but this is actually what's coming from the action. When you return something like View(model), it's returning a ViewResult and inside, it's building this backing class.
The actual cshtml file is just a text file for all intents an purposes. There's a filesystem reference to it (i.e. Path from the IView interface), but that's it. It's not "compiled" per se. The view engine gets fed the View<TModel> instance, with its path to the cshtml file, and then it loads the contents of that file and begins parsing it (Razor), the View<TModel> provides the data backing for that parsing process, i.e. when it comes across Razor code, the information is pull from the View<TModel> instance.
Once the view has been "rendered". The result is basically just a string, which is then dumped into the response. (Technically, there's some async to this, so the response can be and often is progressively built, not just created at the end. However, that's not really germane to the discussion.)

MVC routing priority for shared views

My solution has shared views with similar names in different folder locations
~\Views\Shared\Discount.ascx
~\Views\Dashboard\Shared\Discount.ascx
I'm extending WebFormViewEngine to define a view engine for routing
public class AreaViewEngine : WebFormViewEngine
{
public AreaViewEngine() : base()
{
ViewLocationFormats = new[] {
"~/Views/Shared/{0}.ascx",
"~/Views/Dashboard/Shared/{0}.ascx"
};
MasterLocationFormats = new[] {
"~/Shared/{0}.master"
};
PartialViewLocationFormats = ViewLocationFormats;
}
}
This is causing issues for views with similar names. I want to set higher priority to ~/Views/Dashboard/Shared/{0}.ascx if the URL contains /Dashboard/
Anyone knows how to do it? or is aware of a better way to handle this situation?
You could override the FindView method in your custom ViewEngine class, and look for a match containing the URL fragment before falling back to the default behavior.
public override ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache)
{
var fragment = ... // access URL via controllerContext
var preferred = ViewLocationFormats.Where(x => x.Contains(fragment)).ToList();
// return retult from preferred or fall back to base.FindView
}
I'm not a big fan of this approach, though. If you need a view to be "overridable", you could simply make its output conditional or dependent upon model values, such as by adding a DiscountView property to your view model and then using the value of this property to decide what your view should emit.
If all you desire is "clever naming clash handling" then I'd recommend using T4MVC instead. It allows you to reference your views specifically and without magic strings, resolving any ambiguities that multiple views of the same name might otherwise cause.

System.NullReferenceException creating viewModel

So, I'm trying to find the Umbraco node (as iPublishedContent), and pass it to the viewModel (as Ш've hijacked a route). So i put this in my controller:
private AddCouponCodesViewModel viewModel;
public AddCouponCodesController(){
//Get iPublished content
IPublishedContent content = Umbraco.TypedContent(1225);
//Pass to viewModel
viewModel = new AddCouponCodesViewModel(content);
RouteData.DataTokens["umbraco"] = content;
}
public ActionResult Index()
{
//return view etc
}
But I'm getting
Exception Details: System.NullReferenceException:
Object reference not set to an instance of an object.
here:
Source Error(AddCouponCodesViewModel.cs):
Line 20:
Line 21: }
Line 22: public AddCouponCodesViewModel(IPublishedContent content)
Line 23: : base(content)
Line 24: {
AddCouponCodeRenderModel.cs:
public class AddCouponCodesViewModel : RenderModel
{
public string test { get; set; }
public List<string> tables { get; set; }
public List<string> errors { get; set; }
public AddCouponCodesViewModel(IPublishedContent content, CultureInfo culture) : base(content, culture)
{
}
public AddCouponCodesViewModel(IPublishedContent content)
: base(content)
{
}
And this is the Global.asax
public class Global : UmbracoApplication
{
protected override void OnApplicationStarted(object sender, EventArgs e)
{
base.OnApplicationStarted(sender, e);
BundleConfig.RegisterBundles(BundleTable.Bundles);
//AreaRegistration.RegisterAllAreas();
//WebApiConfig.Register(GlobalConfiguration.Configuration);
//FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
//RouteConfig.RegisterRoutes(RouteTable.Routes);
base.OnApplicationStarting(sender, e);
RouteTable.Routes.MapRoute(
"AddCouponCodes", // Route name
"Admin/{controller}/{action}/{id}", // URL with parameters
new { controller = "AddCouponCodes", action = "Index", id = "" } // Parameter defaults
);
}
}
The content is published (I've checked and double checked), and the node ID is correct.
What I'm basically trying to do here, is to get the route example.com/Admin/{controller}/{action}/{parameter}
To be routed, but having problems connecting it with the umbracoNode (And class RenderModel requires a iPublishContent object as a parameter, but I'm in no luck when trying to pass it anything)
Could someone please help me here, been stuck way too many hours on this :-(
To clarify, if you are hijacking a route, it means that you are overriding the way Umbraco passes it's RenderModel to one of it's published pages. You can either do this globally by overriding the main RenderMvcController, or you can override on a DocumentType-specific basis. So for example, if I have a Homepage doc type, I could create:
public HomepageController : RenderMvcController
{
public override ActionResult Index(RenderModel model)
{
// Create your new renderModel here, inheriting
// from RenderModel
return CurrentTemplate(renderModel);
}
}
This would route all calls to the homepage through this one action. For this, you don't need to define any new routes in the route table. And you should override the render model in the action not in the constructor.
Your question is slightly confusing and it's not entirely clear what you are trying to achieve because:
You have defined a route, and
In your constructor you are calling Umbraco.TypedContent(1225) to retrieve a specific published node
So ... if the admin page you are trying to route has itself been published by Umbraco (and it doesn't sound like it has), the just create a new controller with the name of the page's document type and override the render model in the way described above.
However ... if your admin page hasn't been published by Umbraco and you just want the admin page to access node data, then you have a couple of options:
Create a surface controller, inheriting from SurfaceController. This will give you access to the Umbraco context et al; or
Create a standard controller (preferrably in an Area) and inject the ContentCache using something like Autofac
E.g.:
builder.RegisterControllers(typeof (AdminController).Assembly)
.WithParameter("contentCache", UmbracoContext.Current.ContentCache);
Create a standard controller (preferrably in an Area) and access the node using Umbraco's ContentService API, i.e. new Umbraco.Core.Services.ContentService().GetById(1225)
The difference between the last two approaches is that:
Injecting the ContentCache provides you readonly but very quick access to the published content.
Accessing the ContentService provides you read/write access to the nodes themselves but at the expense of speed as you are querying the database directly.
It depends on what your requirement is.
Either way, it is well worth taking time to read through the documentation for hijacking Umbraco routes, and at least trying to understand what is going on.
Well, I can tell you that your view isn't getting fed anything for the Razor markup because your Index method doesn't feed it anything. That's one problem. I can also tell you, that in your AddCouponCodesViewModel, you'll need an empty constructor, so that the razor syntax can just create an instance, and then populate it to match your submitted object to the view.
Modify your ViewController :
public ActionResult Index()
{
return View(viewModel);
}
Modify your AddCouponCodesViewModel to add an Empty constructor:
public AddCouponCodesViewModel()
{
}
Create a paramaterless constructor on your view model like this:
public AddCouponCodesViewModel():
this(new UmbracoHelper(UmbracoContext.Current).
TypedContent(UmbracoContext.Current.PageId))
{
}
This will get the contexts your other constructors are looking for.
After you've created a class with specific constructors, the compiler stops generating a parameterless one by default. Since you need a parameterless constructor, this is how to get one and still pass in the Umbraco contextual info your viewmodel needs

C# Centralizing repeating VIewData in MVC

When a user log in into my application i want to show his name throughout the whole application. I am using the asp.net MVC framework. But what i don't want is that is have to put in every controller something like:
ViewData["User"] = Session["User"];
This because you may not repeat yourself. (I believe this is the DRY [Don't Repeat Yourself] principle of OO programming.)
The ViewData["User"] is on my masterpage. So my question is, what is a neat way to handle my ViewData["User"] on one place?
You can do this fairly easily in either a controller base-class, or an action-filter that is applied to the controllers/actions. In either case, you get the chance to touch the request before (or after) the action does - so you can add this functionality there.
For example:
public class UserInfoAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(
ActionExecutingContext filterContext)
{
base.OnActionExecuting(filterContext);
filterContext.Controller.ViewData["user"] = "Foo";
}
}
...
[HandleError, UserInfo]
public class HomeController : Controller
{...}
(can also be used at the action (method) level)
or with a common base-class:
public abstract class ControllerBase : Controller
{
protected override void OnActionExecuting(
ActionExecutingContext filterContext)
{
ViewData["user"] = "Bar";
base.OnActionExecuting(filterContext);
}
}
[HandleError]
public class HomeController : ControllerBase
{...}
It's been a year, but I've just stumbled across this question and I believe there's a better answer.
Jimmy Bogard describes the solution described in the accepted answer as an anti-pattern and offers a better solution involving RenderAction: http://www.lostechies.com/blogs/jimmy_bogard/archive/2009/06/18/the-filter-viewdata-anti-pattern.aspx
Another method for providing persistent model data through out your entire application is by overriding the DefaultFactoryController with your custom one. In your CustomerFactoryController, you would hydrate the ViewBag with the model you are wanting to persist.
Create a base class for your models with UserName property:
public abstract class ModelBase
{
public string UserName { get; set; }
}
Create a base class for you controllers and override it's OnActionExecuted method. Within it check if model is derrived from BaseModel and if so, set it's UserName property.
public class ControllerBase : Controller
{
protected override void OnActionExecuted(
ActionExecutedContext filterContext)
{
var modelBase = ViewData.Model as ModelBase;
if (modelBase != null)
{
modelBase.UserName = "foo";
}
base.OnActionExecuted(filterContext);
}
}
Then you will be able to display user's UserName in the view like this:
<%= Html.Encode(Model.UserName) %>
See also:
ASP.NET MVC Best Practices, Tips and Tricks

Categories

Resources