How can I use Razor model binding from a resource string? - asp.net-mvc

Normally we have:
<div>
<div>Some property from a model: #Model.Property</div>
</div>
But let's say I have this exact line as a complete html line in a resource file and try to reference it:
<div>
#Resource.MyResourceLine
</div>
The model binding doesn't work. It renders the line raw without binding.
How can I make Razor bind in this scenario?
EDIT:
There's an alternative way that works by changing the content on the resource string to a string.Format placeholder:
<div>Some property from a model:{0}</div>
and then:
<div>
#string.Format(#Resource.MyResourceLine,#Model.Property)
</div>
But that makes it difficult to maintain large texts with many property references. It would be ideal if the property names could be seen in the resource file. Is there a more elegant way?

I did a bit of digging here and there in the source code for Asp.Net Mvc (latest version which is 5.2.3) taken from official codeplex: https://aspnetwebstack.codeplex.com/
Short answer:
There is no easy way for that out of the box, since the page is already compiled, and any string that you pass in your model is treated like a string - either MvcHtmlString or String. You may use RazorEngine package to do it fast and without lots of issues: (https://github.com/Antaris/RazorEngine)
Long answer:
When you open the route, and controller serves the view for it, you have to take parsed and compiled code for that view(which might get generated during startup, or lazily right before you actually use that view) and then render the page combining compiled View and your Model data (which is done when you call View() method in controller).
How ASP.NET parses and compiles the view, generating running code for it:
// https://aspnetwebstack.codeplex.com/SourceControl/latest#src/System.Web.Razor/RazorTemplateEngine.cs
// Line 152
protected internal virtual GeneratorResults GenerateCodeCore(ITextDocument input, string className, string rootNamespace, string sourceFileName, CancellationToken? cancelToken) {
//...
// Run the parser
RazorParser parser = CreateParser();
Debug.Assert(parser != null);
ParserResults results = parser.Parse(input);
// Generate code
RazorCodeGenerator generator = CreateCodeGenerator(className, rootNamespace, sourceFileName);
generator.DesignTimeMode = Host.DesignTimeMode;
generator.Visit(results);
//...
}
How asp.net renders the page, combining source code for view and data from the model
// https://aspnetwebstack.codeplex.com/SourceControl/latest#src/System.Web.WebPages/WebPageBase.cs
// Line 215
// public override void ExecutePageHierarchy() {
// ...
try
{
// Execute the developer-written code of the WebPage
Execute(); //**you can see example of the code it executes right below in the code block**
}
finally
{
TemplateStack.Pop(Context);
}
}
After the view is compiled, it's turned into a simple C# class that generates a string, that is then displayed to the user in browser. Let's create a simple Controller, View and ViewModel:
Here's some code:
ViewModel class:
namespace StackOverflow.Models
{
public class TestViewModel
{
public int IntProperty { get; set; }
public string StringProperty { get; set; }
}
}
Controller code:
public ActionResult Test()
{
var viewModel = new TestViewModel
{
IntProperty = 5,
StringProperty = "#DateTime.UtcNow.ToString()"
};
return View(viewModel);
}
View:
#model StackOverflow.Models.TestViewModel
#{
ViewBag.Title = "just a test";
Layout = null;
}
#Model.IntProperty
#Html.Raw(#Model.StringProperty)
An example of the page, using the code above, generates the following compiled view:
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace ASP {
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Web;
using System.Web.Helpers;
using System.Web.Security;
using System.Web.UI;
using System.Web.WebPages;
using System.Web.Mvc;
using System.Web.Mvc.Ajax;
using System.Web.Mvc.Html;
using System.Web.Optimization;
using System.Web.Routing;
using StackOverflow;
public class _Page_Views_Test_Index_cshtml : System.Web.Mvc.WebViewPage<StackOverflow.Models.TestViewModel> {
#line hidden
public _Page_Views_Test_Index_cshtml() {
}
protected ASP.global_asax ApplicationInstance {
get {
return ((ASP.global_asax)(Context.ApplicationInstance));
}
}
public override void Execute() {
#line 3 "XXX\Views\Test\Index.cshtml"
ViewBag.Title = "just a test";
Layout = null;
#line default
#line hidden
BeginContext("~/Views/Test/Index.cshtml", 100, 4, true);
WriteLiteral("\r\n\r\n");
EndContext("~/Views/Test/Index.cshtml", 100, 4, true);
BeginContext("~/Views/Test/Index.cshtml", 105, 17, false);
#line 8 "XXX\Views\Test\Index.cshtml"
Write(Model.IntProperty);
#line default
#line hidden
EndContext("~/Views/Test/Index.cshtml", 105, 17, false);
BeginContext("~/Views/Test/Index.cshtml", 122, 4, true);
WriteLiteral("\r\n\r\n");
EndContext("~/Views/Test/Index.cshtml", 122, 4, true);
BeginContext("~/Views/Test/Index.cshtml", 127, 31, false);
#line 10 "XXX\Views\Test\Index.cshtml"
Write(Html.Raw(#Model.StringProperty));
#line default
#line hidden
EndContext("~/Views/Test/Index.cshtml", 127, 31, false);
}
}
}
As you can see, your page code is just written to output section by section, checking Write method leads to these implementation details:
public override void Write(object value)
{
WriteTo(Output, value);
}
public static void WriteTo(TextWriter writer, object content)
{
writer.Write(HttpUtility.HtmlEncode(content)); //writer - instance of TextWriter
}
So anything that you put into your string field in the viewmodel is simply encoded with HtmlEncode method and put to the page, and it cannot be compiled in the run time with default usage of mvc features.
I am really sure, that you can do it with Mvc and Razor digging deep into the sources, but that will require a lot more time and probably a lot of good old hacks. For a fast and simple solution you can use https://github.com/Antaris/RazorEngine package. You can also check its source code for how they did this.
Here's controller code, that will rended a template using RazorEngine package:
public ActionResult Test()
{
var stringTemplate = #"
#model StackOverflow.Models.TestViewModel
<br/>
>>COMPILED
<br/>
#DateTime.UtcNow.ToString()
<br/>
Compiled model int property value:
<br/>
#Model.IntProperty
";
var viewModel = new TestViewModel
{
IntProperty = 5,
StringProperty = null
};
viewModel.StringProperty = Engine.Razor.RunCompile(stringTemplate, viewModel.GetType().ToString(), null, viewModel);
return View("Index", viewModel);
}
Basic idea here is pretty simple - pass the rendering to the component, do it in controller and pass the string you got to the ViewModel, then use #HtmlHelper.Raw to render HTML that you got from the engine.
This might work for a lot scenarios, but I would strongly recommend you not to do it unless you really need it and there are no viable alternatives. Dynamic razor templates are very hard to maintain.

Related

TagHelper never gets executed in .NET Core 2.2 [duplicate]

I've added the following tag helper:
using System;
using System.Linq;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.TagHelpers;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;
namespace X.TagHelpers
{
[HtmlTargetElement(Attributes = ValidationForAttributeName + "," + ValidationErrorClassName)]
public class ValidationClassTagHelper : TagHelper
{
private const string ValidationForAttributeName = "k-validation-for";
private const string ValidationErrorClassName = "k-error-class";
[HtmlAttributeName(ValidationForAttributeName)]
public ModelExpression For { get; set; }
[HtmlAttributeName(ValidationErrorClassName)]
public string ValidationErrorClass { get; set; }
[HtmlAttributeNotBound]
[ViewContext]
public ViewContext ViewContext { get; set; }
public override void Process(TagHelperContext context, TagHelperOutput output)
{
Console.WriteLine("\n\n------------!!!!!!---------\n\n");
ModelStateEntry entry;
ViewContext.ViewData.ModelState.TryGetValue(For.Name, out entry);
if (entry == null || !entry.Errors.Any()) return;
var tagBuilder = new TagBuilder(context.TagName);
tagBuilder.AddCssClass(ValidationErrorClass);
output.MergeAttributes(tagBuilder);
}
}
}
and then in _ViewImports.cshtml I've added the line:
#addTagHelper *, X.TagHelpers
The file is compiled correctly and if I introduce a syntax error dotnet build warns me about it.
Then in one of my pages I add:
<div k-validation-for="OldPassword" k-error-class="has-danger"></div>
If I load the page I see no console output on the server side and the k-validation-for and k-error-class are forwarded to the generated page as is (as opposed to adding the has-danger class to the class attribute).
What am I doing wrong?
When registering Tag Helpers, it’s the assembly that is required, not the namespace - explained in the docs.
...the second parameter "Microsoft.AspNetCore.Mvc.TagHelpers" specifies the assembly containing the Tag Helpers. Microsoft.AspNetCore.Mvc.TagHelpers is the assembly for the built-in ASP.NET Core Tag Helpers.
So in your case, you can just change this:
#addTagHelper *, X.TagHelpers
To this:
#addTagHelper *, X

How to capture content within using block while View is rendering

I am using ASP.NET MVC 3.0, with the ASPX View Engine.
I currently have a method that uses a regex to match certain words within text and highlights them accordingly. So far I am using this to process large amounts of text that are being read from flat files. The end goal I am trying to achieve in this post is to be able to capture content sections of a View, and process them using the same method.
Here is a basic example of how I'm currently trying to achieve this:
<h2>This is a Test</h2>
<p>Line before capture</p>
<% using (Html.CaptureContent())
{ %>
<p>this line should be in capitals</p>
<%} %>
<p>Line after capture</p>
Html.CaptureContent:
public static ContentCapture CaptureContent(this HtmlHelper html)
{
return new ContentCapture(html.ViewContext.HttpContext);
}
ContentCapture:
public class ContentCapture : IDisposable
{
private HttpContextBase Context { get; set; }
private TextWriter OriginalOutput { get; set; }
private StringWriter CaptureOutput { get; set; }
public ContentCapture(HttpContextBase context)
{
CaptureOutput = new StringWriter();
//save the default writer in private property
OriginalOutput = context.Response.Output;
Context = context;
Context.Response.Output = CaptureOutput;
}
public void Dispose()
{
string processedContent = CaptureOutput.ToString().ToUpper();
Context.Response.Output = OriginalOutput;
Context.Response.Output.Write(processedContent);
}
}
When I run this the output is exactly as the tags are in the View, with no processing applied to the <p> tag within the using block. I have tried a couple of variations but with no success. I'm guessing I've made an incorrect assumption of how the View would be rendered as putting a breakpoint in the dispose method has shown me that nothing has been written to the StringWriter object.
Does anyone know of a way I can achieve the desired affect? I'd prefer not to resort to having all the content sections in hard-coded strings being returned by a helper.
You need to write to the ViewContext like this:
htmlHelper.ViewContext.Writer.Write("some content");
So you will have to do some refactoring to use that instead of the HttpContext that you are currently passing through.

Does the standard Html.DisplayTextFor() no HTML encoding?

We are currently dealing with some XSS issues on one of our ASP.NET MVC projects. I found two issues - the first one has to do with our request validation pattern. The attacker could now use this security hole to drop some bad content in our database.
The second issue is how we display this content and we use the Html.DisplayTextFor method and it seems to be "broken".
Just create a new MVC 3 WebApp, put this in the HomeController:
public class HomeController : Controller
{
public ActionResult Index()
{
ViewBag.Message = "<SCRIPT/XSS SRC=\"htpp://ha.ckers.org/css.js\">";
User foo = new User();
foo.Name = "<SCRIPT/XSS SRC=\"htpp://ha.ckers.org/css.js\">";
return View(bla);
}
public ActionResult About()
{
return View();
}
}
public class User
{
public string Name { get; set; }
}
The View:
#Html.TextBoxFor(m => m.Name) <br/> ||| <-- will be encoded
#Html.Encode(ViewBag.Message)<br/> ||| <-- will be double encoded
#Model.Name <br/> ||| <-- will be encoded
#Html.DisplayTextFor(m => m.Name) <-- no encoding
<br/> |||
Output of the DisplayTextFor will be the whole string <script xss="" src="htpp://ha.ckers.org/css.js">
Question is: Bug, feature or am I using it wrong?
Html.DisplayTextFor is really for interacting with the [DisplayFormat] attribute (see MSDN).
So if you're using it with unsafe values, you have to be aware of this and use [DisplayFormat(HtmlEncode = true)] on your property.
Edit: Looks like the HtmlEncode property isn't actually enforced by DataAnnotationsModelMetadataProvider (and DisplayTextFor).

ASP MVC3 - How to load a custom user defined layout for the page from a database?

I have online form builder appplication in ASP.NET MVC3 with Razor views.
It is similar to this - https://examples.wufoo.com/forms/workshop-registration/
I need users to be able to customize the page design.
Not only to upload a custom css, but also to customize the HTML page template.
Let's say users should have complete control on Layout's HTML for their custom webform page. User should be able to edit any HTML on the page, beside the form that is included into the layout.
I'm not sure how to do that with Razor and ASP.NET MVC 3.
Is it possible to:
load layout somewhere from database as string or whatever
replace some custom tags like "FORM1_INCLUDE" to
#Html.Partial("some_non_customizable_layout_for_form1")
use the result as a valid Layout file for the user's form page
Maybe 1-3 is not the best way to do what I need.
What can you suggest for such user defined page layout approach in ASP.NET MVC 3 with Razor views?
UPDATE 1
Using VirtualPathProvider I was able to load View from database, but it just returns text like:
#inherits System.Web.Mvc.WebViewPage
<body>
#Html.EditorFor(z => z.Customer)
</body>
and doesn't process any Razor syntax at all.
What could be the problem with it?
SOLVED:
Needed to place this line as the first one in Application_Start() method:
HostingEnvironment.RegisterVirtualPathProvider(new MyVirtualPathProvider());
UPDATE 2
Custom View Provider is registered in Global.asax.cs as:
protected void Application_Start()
{
HostingEnvironment.RegisterVirtualPathProvider(new MyVirtualPathProvider());
AreaRegistration.RegisterAllAreas();
RegisterGlobalFilters(GlobalFilters.Filters);
RegisterRoutes(RouteTable.Routes);
}
MyVirtualPathProvider code is:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Hosting;
using System.IO;
using System.Text;
namespace new_frontend
{
public class MyVirtualPathProvider : VirtualPathProvider
{
public override bool FileExists(string virtualPath)
{
var td = FindTemplate(virtualPath);
if (td == null)
{
return true;
//return base.FileExists(virtualPath);
}
else
{
return true;
}
}
public override VirtualFile GetFile(string virtualPath)
{
var td = FindTemplate(virtualPath);
if (td == null)
{
return new MyVirtualFile(virtualPath, "");
//return base.GetFile(virtualPath);
}
else
{
return new MyVirtualFile(virtualPath, td.ContentStep1);
}
}
private Facade.Dto.TemplateData FindTemplate(string virtualPath)
{
string prefix = "Template#";
int id = 0;
Facade.Dto.TemplateData td = null;
string fileName = System.IO.Path.GetFileNameWithoutExtension(virtualPath);
if (fileName.StartsWith(prefix))
Int32.TryParse(fileName.Substring(prefix.Length), out id);
if (id > 0)
td = Facade.FrontEndServices.GetTemplate(id);
return td;
}
}
public class MyVirtualFile : VirtualFile
{
private byte[] data;
public MyVirtualFile(string virtualPath, string body)
: base(virtualPath)
{ // 'System.Web.WebPages.ApplicationStartPage
string _body = /*body +*/ #"
#inherits System.Web.Mvc.WebViewPage
#using (Ajax.BeginForm(""Submit"", new AjaxOptions { UpdateTargetId = ""main"" }))
{
}
<!-- <PERSONAL_INFO> -->
<div id=""personal_info"" class=""op2-block"">
</div>
<!-- <PERSONAL_INFO> -->";
this.data = Encoding.UTF8.GetBytes(_body);
}
public override System.IO.Stream Open()
{
return new MemoryStream(data);
}
}
}
And now for the Razor view code defined as a string above I get this exception:
"Compiler Error Message: CS1061: 'System.Web.Mvc.AjaxHelper' does not contain a definition for 'BeginForm' and no extension method 'BeginForm' accepting a first argument of type 'System.Web.Mvc.AjaxHelper' could be found (are you missing a using directive or an assembly reference?)"
And when I changed Razor View code to:
string _body = /*body +*/ #"
#using System.Web.WebPages;
#using System.Web.Mvc;
#using System.Web.Mvc.Ajax;
#using System.Web.Mvc.Html;
#using System.Web.Routing;
#inherits System.Web.Mvc.WebViewPage<dynamic>
#using (Ajax.BeginForm(""Submit"", new AjaxOptions { UpdateTargetId = ""main"" }))
{
}
<!-- <PERSONAL_INFO> -->
<div id=""ppg_op2_personal_info"" class=""op2-block"">
</div>
<!-- <PERSONAL_INFO> -->";
I got a different error:
Type 'ASP._Page__appstart_cshtml' does not inherit from 'System.Web.WebPages.ApplicationStartPage'
When I change
#inherits System.Web.Mvc.WebViewPage
to
#inherits System.Web.WebPages.ApplicationStartPage
to fix the error above, I get a new one:
"Compiler Error Message: CS0103: The name 'Ajax' does not exist in the current context"
UPDATE3:
I tried to use base.XXX:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Hosting;
using System.IO;
using System.Text;
namespace new_frontend
{
public class MyVirtualPathProvider : VirtualPathProvider
{
public override bool FileExists(string virtualPath)
{
var td = FindTemplate(virtualPath);
if (td == null)
{
//return true;
return base.FileExists(virtualPath);
}
else
{
return true;
}
}
public override VirtualFile GetFile(string virtualPath)
{
var td = FindTemplate(virtualPath);
if (td == null)
{
//return new MyVirtualFile(virtualPath, "");
return base.GetFile(virtualPath);
}
else
{
return new MyVirtualFile(virtualPath, td.ContentStep1);
}
}
private Facade.Dto.TemplateData FindTemplate(string virtualPath)
{
string prefix = "Template#";
int id = 0;
Facade.Dto.TemplateData td = null;
string fileName = System.IO.Path.GetFileNameWithoutExtension(virtualPath);
if (fileName.StartsWith(prefix))
Int32.TryParse(fileName.Substring(prefix.Length), out id);
if (id > 0)
td = Facade.FrontEndServices.GetTemplate(id);
return td;
}
}
public class MyVirtualFile : VirtualFile
{
private byte[] data;
public MyVirtualFile(string virtualPath, string body)
: base(virtualPath)
{ // 'System.Web.WebPages.ApplicationStartPage
string _body = /*body +*/ #"
#inherits System.Web.Mvc.WebViewPage<PPG.Facade.Dto.NewOrderPageData>
#using (Ajax.BeginForm(""Submit"", new AjaxOptions { UpdateTargetId = ""main"" }))
{
}
<!-- <PERSONAL_INFO> -->
<div id=""personal_info"" class=""op2-block"">
</div>
<!-- <PERSONAL_INFO> -->";
this.data = Encoding.UTF8.GetBytes(_body);
}
public override System.IO.Stream Open()
{
return new MemoryStream(data);
}
}
}
In this case I get a view that is not parsed at all, this is what I get in the web browser:
#using System.Web.WebPages;
#using System.Web.Mvc;
#using System.Web.Mvc.Ajax;
#using System.Web.Mvc.Html;
#using System.Web.Routing;
#inherits System.Web.Mvc.WebViewPage<PPG.Facade.Dto.NewOrderPageData>
#using (Ajax.BeginForm("Submit", new AjaxOptions { UpdateTargetId = "main" }))
{
}
<!-- <PERSONAL_INFO> -->
<div id="ppg_op2_personal_info" class="op2-block">
</div>
<!-- <PERSONAL_INFO> -->
You should create a virtual path provider which fetches your custom views from a database.
There are several questions here about them. Just search for VirtualPathProvider
Updates (from my comments to the question discussion)
The VirtualPathProvider must be registered in Application_Start using HostingEnvironment.RegisterVirtualPathProvider(new MyVirtualPathProvider());
The base class MUST be called for all files that you can't currently serve. This is required since there can only be one VirtualPathProvider. (You'll see lots of strange errors otherwise)
#model directive doesn't work in files that you serve. You must use #inherits System.Web.Mvc.WebViewPage<YourNameSpace.YourModelName> instead.
IIRC you also need to override GetCacheDependency and return null for your own resources.
depending on the degree of flexibility and customization you wish to give this can be a very simple to a very time consuming task.
If you want users to simply customize the HTML on the page then you can use a WYSIWYG editor and store the raw html in the database.
In the view, use
#Html.Raw(Model.body) // Where body is the field containing the wysiwyg content
This will render the markup as-is.
To include custom tag / replacement you will have to define a list of string constants that can be inserted while using the WYSIWYG editor. These can then be search - replaced when displaying.
For example in your controller or model:
model.body.replace("$[form1]", "<form action='something' method='post' name='form1'></form>");
Of course depending on the nature of your application you might want to re-factor this into some sort of tag => markup conversion which will allow you to add more custom tags and their respective real HTML markups.
Hope this helps, Cheers!
I think you have to use something like this:
http://vibrantcode.com/blog/2010/11/16/hosting-razor-outside-of-aspnet-revised-for-mvc3-rc.html
I've done something similar & you may fine that XML is very useful. You save the layout definition as XML to the DB. That means it's easy to manipulate, either directly or by serialise/deserialise into an object model.
When you want to display the page, use XSLT to transform your XML into HTML, applying styles etc to the output.

Generic Inherited ViewPage<> and new Property

Setup:
CustomViewEngine
CustomController Base
CustomViewPage Base (in this base, a new property is added "MyCustomProperty")
Problem:
When a view is strongly typed such as: <# Page Inherits="CustomViewPage<MyCustomObject" MyCustomProperty="Hello">, I get a compiler "Parser" error stating that MyCustomProperty is not a public property of System.Web.Mvc.ViewPage
I have done numerous trial and errors (see below) to see whats causing this error and have come to the following conclusions:
The error only occurs when I declare "MyCustomProperty" or any other property in the #Page directive of the view.
The error will always display "System.Web.Mvc.ViewPage" rather than the declared inherits=".." class.
Update: Looks like Technitium found another way to do this that looks much easier, at least on newer versions of ASP.NET MVC. (copied his comment below)
I'm not sure if this is new in ASP.NET MVC 3, but when I swapped the
Inherits attribute from referencing the generic in C# syntax to CLR
syntax, the standard ViewPageParserFilter parsed generics correctly --
no CustomViewTypeParserFilter required. Using Justin's examples, this
means swapping
<%# Page Language="C#" MyNewProperty="From #Page directive!"
Inherits="JG.ParserFilter.CustomViewPage<MvcApplication1.Models.FooModel>
to
<%# Page Language="C#" MyNewProperty="From #Page directive!"`
Inherits="JG.ParserFilter.CustomViewPage`1[MvcApplication1.Models.FooModel]>
Original answer below:
OK, I solved this. Was a fascinating exercise, and the solution is non-trivial but not too hard once you get it working the first time.
Here's the underlying issue: the ASP.NET page parser does not support generics as a page type.
The way ASP.NET MVC worked around this was by fooling the underlying page parser into thinking that the page is not generic. They did this by building a custom PageParserFilter and a custom FileLevelPageControlBuilder. The parser filter looks for a generic type, and if it finds one, swaps it out for the non-generic ViewPage type so that the ASP.NET parser doesn't choke. Then, much later in the page compilation lifecycle, their custom page builder class swaps the generic type back in.
This works because the generic ViewPage type derives from the non-generic ViewPage, and all the interesting properties that are set in a #Page directive exist on the (non-generic) base class. So what's really happening when properties are set in the #Page directive is that those property names are being validated against the non-generic ViewPage base class.
Anyway, this works great in most cases, but not in yours because they hardcode ViewPage as the non-generic base type in their page filter implementation and don't provide an easy way to change it. This is why you kept seeing ViewPage in your error message, since the error happens in between when ASP.NET swaps in the ViewPage placeholder and when it swaps back the generic ViewPage right before compilation.
The fix is to create your own version of the following:
page parser filter - this is almost an exact copy of ViewTypeParserFilter.cs in the MVC source, with the only difference being that it refers to your custom ViewPage and page builder types instead of MVC's
page builder - this is identical to ViewPageControlBuilder.cs in the MVC source, but it puts the class in your own namespace as opposed to theirs.
Derive your custom viewpage class directly from System.Web.Mvc.ViewPage (the non-generic version). Stick any custom properties on this new non-generic class.
derive a generic class from #3, copying the code from the ASP.NET MVC source's implementation of ViewPage.
repeat #2, #3, and #4 for user controls (#Control) if you also need custom properties on user control directives too.
Then you need to change the web.config in your views directory (not the main app's web.config) to use these new types instead of MVC's default ones.
I've enclosed some code samples illustrating how this works. Many thanks to Phil Haack's article to help me understand this, although I had to do a lot of poking around the MVC and ASP.NET source code too to really understand it.
First, I'll start with the web.config changes needed in your web.config:
<pages
validateRequest="false"
pageParserFilterType="JG.ParserFilter.CustomViewTypeParserFilter"
pageBaseType="JG.ParserFilter.CustomViewPage"
userControlBaseType="JG.ParserFilter.CustomViewUserControl">
Now, here's the page parser filter (#1 above):
namespace JG.ParserFilter {
using System;
using System.Collections;
using System.Web.UI;
using System.Web.Mvc;
internal class CustomViewTypeParserFilter : PageParserFilter
{
private string _viewBaseType;
private DirectiveType _directiveType = DirectiveType.Unknown;
private bool _viewTypeControlAdded;
public override void PreprocessDirective(string directiveName, IDictionary attributes) {
base.PreprocessDirective(directiveName, attributes);
string defaultBaseType = null;
// If we recognize the directive, keep track of what it was. If we don't recognize
// the directive then just stop.
switch (directiveName) {
case "page":
_directiveType = DirectiveType.Page;
defaultBaseType = typeof(JG.ParserFilter.CustomViewPage).FullName; // JG: inject custom types here
break;
case "control":
_directiveType = DirectiveType.UserControl;
defaultBaseType = typeof(JG.ParserFilter.CustomViewUserControl).FullName; // JG: inject custom types here
break;
case "master":
_directiveType = DirectiveType.Master;
defaultBaseType = typeof(System.Web.Mvc.ViewMasterPage).FullName;
break;
}
if (_directiveType == DirectiveType.Unknown) {
// If we're processing an unknown directive (e.g. a register directive), stop processing
return;
}
// Look for an inherit attribute
string inherits = (string)attributes["inherits"];
if (!String.IsNullOrEmpty(inherits)) {
// If it doesn't look like a generic type, don't do anything special,
// and let the parser do its normal processing
if (IsGenericTypeString(inherits)) {
// Remove the inherits attribute so the parser doesn't blow up
attributes["inherits"] = defaultBaseType;
// Remember the full type string so we can later give it to the ControlBuilder
_viewBaseType = inherits;
}
}
}
private static bool IsGenericTypeString(string typeName) {
// Detect C# and VB generic syntax
// REVIEW: what about other languages?
return typeName.IndexOfAny(new char[] { '<', '(' }) >= 0;
}
public override void ParseComplete(ControlBuilder rootBuilder) {
base.ParseComplete(rootBuilder);
// If it's our page ControlBuilder, give it the base type string
CustomViewPageControlBuilder pageBuilder = rootBuilder as JG.ParserFilter.CustomViewPageControlBuilder; // JG: inject custom types here
if (pageBuilder != null) {
pageBuilder.PageBaseType = _viewBaseType;
}
CustomViewUserControlControlBuilder userControlBuilder = rootBuilder as JG.ParserFilter.CustomViewUserControlControlBuilder; // JG: inject custom types here
if (userControlBuilder != null) {
userControlBuilder.UserControlBaseType = _viewBaseType;
}
}
public override bool ProcessCodeConstruct(CodeConstructType codeType, string code) {
if (codeType == CodeConstructType.ExpressionSnippet &&
!_viewTypeControlAdded &&
_viewBaseType != null &&
_directiveType == DirectiveType.Master) {
// If we're dealing with a master page that needs to have its base type set, do it here.
// It's done by adding the ViewType control, which has a builder that sets the base type.
// The code currently assumes that the file in question contains a code snippet, since
// that's the item we key off of in order to know when to add the ViewType control.
Hashtable attribs = new Hashtable();
attribs["typename"] = _viewBaseType;
AddControl(typeof(System.Web.Mvc.ViewType), attribs);
_viewTypeControlAdded = true;
}
return base.ProcessCodeConstruct(codeType, code);
}
// Everything else in this class is unrelated to our 'inherits' handling.
// Since PageParserFilter blocks everything by default, we need to unblock it
public override bool AllowCode {
get {
return true;
}
}
public override bool AllowBaseType(Type baseType) {
return true;
}
public override bool AllowControl(Type controlType, ControlBuilder builder) {
return true;
}
public override bool AllowVirtualReference(string referenceVirtualPath, VirtualReferenceType referenceType) {
return true;
}
public override bool AllowServerSideInclude(string includeVirtualPath) {
return true;
}
public override int NumberOfControlsAllowed {
get {
return -1;
}
}
public override int NumberOfDirectDependenciesAllowed {
get {
return -1;
}
}
public override int TotalNumberOfDependenciesAllowed {
get {
return -1;
}
}
private enum DirectiveType {
Unknown,
Page,
UserControl,
Master,
}
}
}
Here's the page builder class (#2 above):
namespace JG.ParserFilter {
using System.CodeDom;
using System.Web.UI;
internal sealed class CustomViewPageControlBuilder : FileLevelPageControlBuilder {
public string PageBaseType {
get;
set;
}
public override void ProcessGeneratedCode(
CodeCompileUnit codeCompileUnit,
CodeTypeDeclaration baseType,
CodeTypeDeclaration derivedType,
CodeMemberMethod buildMethod,
CodeMemberMethod dataBindingMethod) {
// If we find got a base class string, use it
if (PageBaseType != null) {
derivedType.BaseTypes[0] = new CodeTypeReference(PageBaseType);
}
}
}
}
And here's the custom view page classes: the non-generic base (#3 above) and the generic derived class (#4 above):
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Diagnostics.CodeAnalysis;
using System.Web.Mvc;
namespace JG.ParserFilter
{
[FileLevelControlBuilder(typeof(JG.ParserFilter.CustomViewPageControlBuilder))]
public class CustomViewPage : System.Web.Mvc.ViewPage //, IAttributeAccessor
{
public string MyNewProperty { get; set; }
}
[FileLevelControlBuilder(typeof(JG.ParserFilter.CustomViewPageControlBuilder))]
public class CustomViewPage<TModel> : CustomViewPage
where TModel : class
{
// code copied from source of ViewPage<T>
private ViewDataDictionary<TModel> _viewData;
public new AjaxHelper<TModel> Ajax
{
get;
set;
}
public new HtmlHelper<TModel> Html
{
get;
set;
}
public new TModel Model
{
get
{
return ViewData.Model;
}
}
[SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
public new ViewDataDictionary<TModel> ViewData
{
get
{
if (_viewData == null)
{
SetViewData(new ViewDataDictionary<TModel>());
}
return _viewData;
}
set
{
SetViewData(value);
}
}
public override void InitHelpers()
{
base.InitHelpers();
Ajax = new AjaxHelper<TModel>(ViewContext, this);
Html = new HtmlHelper<TModel>(ViewContext, this);
}
protected override void SetViewData(ViewDataDictionary viewData)
{
_viewData = new ViewDataDictionary<TModel>(viewData);
base.SetViewData(_viewData);
}
}
}
And here are the corresponding classes for user controls (#5 above) :
namespace JG.ParserFilter
{
using System.Diagnostics.CodeAnalysis;
using System.Web.Mvc;
using System.Web.UI;
[FileLevelControlBuilder(typeof(JG.ParserFilter.CustomViewUserControlControlBuilder))]
public class CustomViewUserControl : System.Web.Mvc.ViewUserControl
{
public string MyNewProperty { get; set; }
}
public class CustomViewUserControl<TModel> : CustomViewUserControl where TModel : class
{
private AjaxHelper<TModel> _ajaxHelper;
private HtmlHelper<TModel> _htmlHelper;
private ViewDataDictionary<TModel> _viewData;
public new AjaxHelper<TModel> Ajax {
get {
if (_ajaxHelper == null) {
_ajaxHelper = new AjaxHelper<TModel>(ViewContext, this);
}
return _ajaxHelper;
}
}
public new HtmlHelper<TModel> Html {
get {
if (_htmlHelper == null) {
_htmlHelper = new HtmlHelper<TModel>(ViewContext, this);
}
return _htmlHelper;
}
}
public new TModel Model {
get {
return ViewData.Model;
}
}
[SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
public new ViewDataDictionary<TModel> ViewData {
get {
EnsureViewData();
return _viewData;
}
set {
SetViewData(value);
}
}
protected override void SetViewData(ViewDataDictionary viewData) {
_viewData = new ViewDataDictionary<TModel>(viewData);
base.SetViewData(_viewData);
}
}
}
namespace JG.ParserFilter {
using System.CodeDom;
using System.Web.UI;
internal sealed class CustomViewUserControlControlBuilder : FileLevelUserControlBuilder {
internal string UserControlBaseType {
get;
set;
}
public override void ProcessGeneratedCode(
CodeCompileUnit codeCompileUnit,
CodeTypeDeclaration baseType,
CodeTypeDeclaration derivedType,
CodeMemberMethod buildMethod,
CodeMemberMethod dataBindingMethod) {
// If we find got a base class string, use it
if (UserControlBaseType != null) {
derivedType.BaseTypes[0] = new CodeTypeReference(UserControlBaseType);
}
}
}
}
Finally, here's a sample View which shows this in action:
<%# Page Language="C#" MyNewProperty="From #Page directive!" Inherits="JG.ParserFilter.CustomViewPage<MvcApplication1.Models.FooModel>" %>
<%=Model.SomeString %>
<br /><br />this.MyNewPrroperty = <%=MyNewProperty%>
</asp:Content>

Resources