Extending WebFormView in MVC - asp.net-mvc

I want to extend the WebFormViewEngine so that I can perform some post-processing - I want it to do it's stuff, then hand me the Html back, so I can do put some final touches to it. It needs to be done as a View/ViewEngine because I need access to the ViewData.
Unfortunately there seems to be no way to get the Html back from the WebFormView, and no way to hand a custom HtmlTextWriter to the WebFormView or ViewPage.
Surely there's a way to do this? No?
Littlecharva

You can use Action Filters to do this. Check out this tutorial at asp.net/mvc. You want to use a ResultsFilter.
As an alternate, you can override the virtual OnResultExecuted method of the Controller.

You can capture output recevier before the View gets rendered by overriding Render method of the WebFormView class. The trick is that the output receiver is not the System.IO.TextWriter writer but the Writer property of the viewContext.
Also, you have to extend WebFormViewEngine to return your views.
public class MyViewEngine : WebFormViewEngine
{
protected override IView CreatePartialView(ControllerContext controllerContext, string partialPath)
{
return new MyView(partialPath, null);
}
protected override IView CreateView(ControllerContext controllerContext, string viewPath, string masterPath)
{
return new MyView(viewPath, masterPath);
}
}
public class MyView : WebFormView
{
public MyView(string inViewPath, string inMasterPath) : base(inViewPath, inMasterPath) { }
public MyView(string inViewPath) : base(inViewPath) { }
public override void Render(ViewContext viewContext, System.IO.TextWriter writer)
{
//make a switch to custom output receiver
var oldWriter = viewContext.Writer;
viewContext.Writer = new System.IO.StringWriter();
base.Render(viewContext, null);
viewContext.Writer.Close();
//get output html
var html = ((System.IO.StringWriter)viewContext.Writer).GetStringBuilder();
//perform processing
html.Replace('a', 'b');
//retransmit output
viewContext.Writer = oldWriter;
viewContext.Writer.Write(html);
}
}

Okay, I have never done this before but I looked through reflector and the MVC assemblies. It appears as though you it might be possible to extend the ViewPage and the ViewPage and the ViewMasterPage object with your object. The in your own object you can override the render method and get a handle tot the HtmlTextWriter. Then just pass it on to the base and let it do it's thing. Something like this (this is un-tested and is only theoretical, there may be more methods you need to override.) I recommend using reflector to see how it is done now and even how other view engines like Spark do it.
public class MyPage : ViewPage
{
protected override void Render(System.Web.UI.HtmlTextWriter writer)
{
//Do custom stuff here
base.Render(writer);
}
}
public class MyPage<TModel> : MyPage where TModel : class
{
}

Related

Asp.NET MVC ModelBinder, getting Action Method

I got a custom ModelBinder and i would like to get the action. Because i want to get the Attributes of the action using reflection, the action name is not enough.
my action method:
[MyAttribute]
public ActionResult Index([ModelBinder(typeof(MyModelBinder))] MyModel model)
{
}
and here a typically ModelBinder
public class MyModelBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
// here i would like to get the action method and his "MyAttribute"
}
}
any suggestions, other solutions ?
many thanks in advance
No, you cannot with 100% certainty get the current action from a model binder. The model binder is not coupled to the action, but to binding to a model. For example, you can call
TryUpdateMode(model)
In an filter before an action has been chosen. Also note that an action method might not even be a CLR method (see http://haacked.com/archive/2009/02/17/aspnetmvc-ironruby-with-filters.aspx) that can be reflected on.
I think the real question is, what exactly are you trying to accomplish and is this the right way? If you want information from the action to be passed to the model binder (heeding the advice that your model binder should degrade gracefully if the information isn't there), you should use an action filter to put the information in HttpContext.Items (or somewhere like that) and then have your binder retrieve it.
An action filter's OnActionExecuting method receives an ActionExecutingContext which has an ActionDescriptor. You can call GetCustomAttributes on that.
You could try this:
var actionName = controllerContext.RouteData.GetRequiredString("action");
var myAttribute = (MyAttribute) Attribute.GetCustomAttribute(controllerContext.Controller.GetMethod(actionName), typeof(MyAttribute));
You could override ControllerActionInvoker.FindAction() to get the action's attribute and store it in HttpContext.Current.Items as mentioned here, or extendedControllerContext.RequestContext, as follows:
public class MyControllerActionInvoker : ControllerActionInvoker
{
protected override ActionDescriptor FindAction(ControllerContext controllerContext, ControllerDescriptor controllerDescriptor, string actionName)
{
var action = base.FindAction(controllerContext, controllerDescriptor, actionName);
if (action != null)
{
var requestContext = ExtendedRequestContext.Bind(controllerContext);
var attr = action.GetCustomAttributes(typeof(MyAttribute), false).FirstOrDefault();
if (attr != null)
requestContext.CustomAttribute = (MyAttribute)attr;
}
return action;
}
}
public class ExtendedRequestContext : RequestContext
{
public MyAttribute CustomAttribute { get; set; }
public static ExtendedRequestContext Bind(ControllerContext controllerContext)
{
var requestContext = new ExtendedRequestContext
{
HttpContext = controllerContext.RequestContext.HttpContext,
RouteData = controllerContext.RequestContext.RouteData
};
controllerContext.RequestContext = requestContext;
return requestContext;
}
}
The default action invoker is replaced either in your controller's constructor or in a custom controllers factory:
public MyController() : base()
{
ActionInvoker = new MyControllerActionInvoker();
}
By the way, Controller.TempData already contains an item of ReflectedParameterDescriptor type, which gives you access to ActionDescriptor, so the above code may be redundant. However, beware this is implementation specific, so may change over time.
Finally, get the attribute from that storage in your binder class:
var requestContext = (ExtendedRequestContext)controllerContext.RequestContext;
if (requestContext.CustomAttribute != null)
{
// apply your logic here
}

ASP.NET MVC: Enforce AJAX request on an action

I'm looking for a way to enforce a controller's action to be accessed only via an AJAX request.
What is the best way to do this before the action method is called? I want to refactor the following from my action methods:
if(Request.IsAjaxRequest())
// Do something
else
// return an error of some sort
What I'm envisioning is an ActionMethodSelectorAttribute that can be used like the [AcceptVerbs] attribute. I have no experience crating such a custom attribute though.
Create an ActionFilter that fires OnActionExecuting
public class AjaxActionFilter : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
if (!filterContext.HttpContext.Request.IsAjaxRequest())
filterContext.Result = new RedirectResult(//path to error message);
}
}
Setting the filter's Result property will prevent execution of the ActionMethod.
You can then apply it as an attribute to your ActionMethods.
Its as simple as this:
public class AjaxOnly : ActionMethodSelectorAttribute
{
public override bool IsValidForRequest(ControllerContext controllerContext, System.Reflection.MethodInfo methodInfo)
{
return controllerContext.HttpContext.IsAjaxRequest();
}
}
I just forget where IsAjaxRequest() comes from, I'm pasting from code I have but "lost" that method. ;)

Custom IIdentity and passing data from an attribute to a controller

Here's my scenario:
I've successfully created a custom IIdentity that I pass to a GenericPrincipal. When I access that IIdentity in my controller I have to cast the IIdentity in order to use the custom properties. example:
public ActionResult Test()
{
MyCustomIdentity identity = (MyCustomIdentity)User.Identity;
int userID = identity.UserID;
...etc...
}
Since I need to do this casting for nearly every action I would like to wrap this functionality in an ActionFilterAttribute. I can't do it in the controller's constructor because the context isn't initialized yet. My thought would be to have the ActionFilterAttribute populate a private property on the controller that I can use in each action method. example:
public class TestController : Controller
{
private MyCustomIdentity identity;
[CastCustomIdentity]
public ActionResult()
{
int userID = identity.UserID;
...etc...
}
}
Question: Is this possible and how? Is there a better solution? I've racked my brain trying to figure out how to pass public properties that are populated in an attribute to the controller and I can't get it.
All you have to do is access the ActionExecutingContext of an overloaded OnActionExecuting() method and make identity public instead of private so your actionfilter can access it.
public class CastCustomIdentity : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
((TestController) filterContext.Controller).Identity = (MyCustomIdentity)filterContext.HttpContext.User;
base.OnActionExecuting(filterContext);
}
}
This could be even easier by using a custom base controller class that all of your controllers would inherit from:
public class MyCustomController
{
protected MyCustomIdentity Identity { get{ return (MyCustomIdentity)User.Identity; } }
}
and then:
public class TestController : MyCustomController
{
public ActionResult()
{
int userID = Identity.UserId
...etc...
}
}
You could use a custom model binder...
I can't remember why I used this method over the base controller method #jfar mentions (which is also a good option), but it works well for me and I actually kinda like it because my actions are more self describing through their parameters.
MyCustomIdentityModelBinder.cs
public class MyCustomIdentityModelBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
if (bindingContext.Model != null)
throw new InvalidOperationException("Cannot update instances");
//If the user isn't logged in, return null
if (!controllerContext.HttpContext.User.Identity.IsAuthenticated)
return null;
return controllerContext.HttpContext.User as MyCustomIdentity;
}
}
Inside your application start event in Global.asax.cs
System.Web.Mvc.ModelBinders.Binders.Add(typeof(MyCustomIdentity), new MyCustomIdentityModelBinder());
Then whenever you have a type of MyCustomIdentity as an action parameter, it'll automatically use the MyCustomIdentityModelBinder.
Eg.
public class TestController : Controller
{
public ActionResult Index(MyCustomIdentity identity)
{
int userID = identity.UserID;
...etc...
}
}
HTHs,
Charles

Chose to render different View (ASPX) file in ASP.NET MVC

I have this
public class HomeController{
public ActionResult Index()
{
//do stuff
return View();
}
Obviously this choses and renders Index.aspx in the Home folder.
What we really want is to chose another file - Index.ar.aspx - if the CurrentCulture is ar-AE. I don't want IF statements on every return View() call. Anyone help me find the best place to override the name of the view file that is selected?
Note, please don't tell me off :) I know that separate files are a bit hacky, and we ARE using RESX files, DIR directives and routes to change languages etc. But we need seperate files for layout reasons.
You should create your own ViewEngine. If you are using the WebFormViewEngine that is the default one with MVC, you could easily subclass it and then override the FindView(...) method.
In your overridden FindView(...) method you could easily look for a file based on a convention that includes the name of the current culture.
Take a look at Scott Hanselmans post about a ViewEngine that looks for different view files if the site is browsed using a mobile device.
Perhaps something like:
public class ExampleViewEngine : WebFormViewEngine
{
public override ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache)
{
ViewEngineResult result = null;
string conventionViewName = string.Format("{0}.{1}", viewName, System.Globalization.CultureInfo.CurrentCulture.TwoLetterISOLanguageName);
result = base.FindView(controllerContext, conventionViewName, masterName, useCache);
if (result == null || result.View == null)
{
result = base.FindView(controllerContext, viewName, masterName, useCache);
}
return result;
}
}
It sounds like you really want the View engine to be able to decide which view to return, rather than having the controllers be responsible for it.
Take a look at this tutorial, and google around for some others. It's pretty simple to override the default view engine, and you can add the language choosing logic there, removing the need for it at the controller level.
public class BaseController{
// Don't remember parameter type exactly
public void OnActionExecuted(ActionExecutedContext context)
{
// if view is returned, add culture suffix to its name
// also may need to do so for PartialViewResult
// One problem is if view is not named; Name is "";
// in this case use context.ActionContext.Name or RouteData["action"] for view name
if (context.Result is ViewResult)
{
var view = context.Result as ViewResult;
view.Name = view.Name + CurrentCultureSuffix;
}
}
}
public class HomeController: BaseController{
public ActionResult Index()
{
//do stuff
return View();
}
}

Unit testing Controller.Initialize

I am setting the thread culture to the culture sent via a cookie on the HttpRequest in the initialize method of a base controller I have created. I now want to create a unit test for this functionality.
In the test I have created a mock HttpContext and added the Cookie. Using this and a routeData helper, I create a RequestContext. Then I create a new controller and call Execute on it passing in the RequestContext.
I first hit a problem regarding TempData and SessionState, which I have fixed by setting the TempData and creating an EmptyTempDataProvider on the controller.
Then I had a problem with the VirtualPathProviderViewengine, which I fixed by creating a VoidActionInvoker.
Is this the best way to test the initialize method? Any doing anything similar and willing to share some code?
Thanks,
Jon
namespace MyApp.UnitTests {
[TestClass]
public class ControllerTests {
[TestInitialize]
public void Setup() {
RouteTable.Routes.Clear();
MyApp.MvcApplication.RegisterRoutes(RouteTable.Routes);
}
[TestMethod]
public void Thread_culture_should_be_set_to_cookie_culture() {
// Arrange
var context = HttpMockHelper.FakeHttpContext();
context.Request.Cookies.Add(new HttpCookie(CultureService.CookieName, "nl-NL"));
var reqContext = new RequestContext(context, "~/Home/Landing".Route());
var controller = new HomeController {
TempData = new TempDataDictionary(),
TempDataProvider = new EmptyTempDataProvider(),
ActionInvoker = new VoidActionInvoker()
};
// Act
(controller as IController).Execute(reqContext);
// Assert
Assert.AreEqual("nl-NL", Thread.CurrentThread.CurrentCulture.Name);
}
}
internal class VoidActionInvoker : ControllerActionInvoker {
protected override ActionExecutedContext InvokeActionMethodWithFilters(System.Reflection.MethodInfo methodInfo, IDictionary<string, object> parameters, IList<IActionFilter> filters) {
return new ActionExecutedContext(this.ControllerContext, false, null);
}
protected override void InvokeActionResult(ActionResult actionResult) {
}
}
internal class EmptyTempDataProvider : ITempDataProvider {
public void SaveTempData(ControllerContext controllerContext, IDictionary<string, object> values) {
}
public IDictionary<string, object> LoadTempData(ControllerContext controllerContext) {
return new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
}
}
}
Sounds right to me, the initialize method takes a parameter for the requestcontext that requires an HttpContext, if I've got your post right you're mocking these objects and adding the values you want to test with and then ensuring that your method works correctly. Sounds exactly like what I would do. I wouldn't mind seeing some of the code though.
Edit: Your tests look good to me.

Resources