Can't use App.xaml resources after Dependency Injection .NET MAUI - dependency-injection

I have an application that uses mvvm pattern with Community Toolkit features. I am trying to use Dependency Injection to use an ApiService with its interface in my viewmodels, but after following the steps described here, I can't access to App.xaml Resources (specifically colors), the intellisense works when I am writing code in XAML, but it doesn´t work after running. It is important to notice that I was using colors from resources correctly before trying to use Dependency Injection and changing the ViewModel - View linking method to the one described here, but I was unable to use ApiService.
Here is my code.
App.xaml.cs (Login page is my first page):
public App(LoginPage page)
{
InitializeComponent();
MainPage = page;
}
LoginPage.xaml.cs
public partial class LoginPage : ContentPage
{
public LoginPage(LoginPageViewModel loginPageViewModel)
{
BindingContext = loginPageViewModel;
InitializeComponent();
}
}
LoginPageViewModel
public LoginPageViewModel(IApiService apiService)
{
_apiService = apiService;
Opacity = 1f;
IsEnabled = true;
IsRunning = false;
}
LoginPage.xaml
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="NewScholarApp.Views.LoginPage"
Title="LoginPage"
xmlns:vm="clr-namespace:NewScholarApp.ViewModels"
x:DataType="vm:LoginPageViewModel"
BackgroundColor="{StaticResource GreenSchool}">
MauiProgam.cs
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>().UseMauiCommunityToolkit().ConfigureViewModels().ConfigureServices().ConfigureViews()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
});
//builder.Services.AddSingleton<ApiService>();
return builder.Build();
}
RegisterViews and ViewModels
public static MauiAppBuilder ConfigureViews(this MauiAppBuilder mauiAppBuilder)
{
mauiAppBuilder.Services.AddTransient<LoginPage>();
return mauiAppBuilder;
}
public static class RegisterViewModels
{
public static MauiAppBuilder ConfigureViewModels(this MauiAppBuilder mauiAppBuilder)
{
mauiAppBuilder.Services.AddSingleton<LoginPageViewModel>();
return mauiAppBuilder;
}
}
Open issue: https://github.com/dotnet/maui/issues/11485

This question is related to Cannot use StaticResources for resources in App.xaml. IndianaGary's comment indicated that you could try a DynamicResource.

Related

Asp.Net Core Dependency Injection through Annotation like Spring MVC

How can i inject using annotation which is like a feature of Spring MVC
public class AccountController :Controller
{
[Injected] // Need like this annotation
private AccountService _accountService;
public AccountController(){
}
}
You may use Unity Container
Here is an example:
public class MyObject
{
private SomeOtherObject _dependentObject;
[Dependency]
public SomeOtherObject DependentObject
{
get { return _dependentObject; }
set { _dependentObject = value; }
}
}
And here is the resolution:
IUnityContainer uContainer = new UnityContainer();
MyObject myInstance = uContainer.Resolve<MyObject>();
// now access the property containing the dependency
SomeOtherObject depObj = myInstance.DependentObject;
In .Net core application no need to mention as injected annotation.you need to change your service as interface driven.
In solution startup page add below shown code before services.AddMvc();
services.AddTransient<IInterface, Service>();
and in controller make change as
public class AccountController :Controller
{
private IAccountService _accountService;
public AccountController(IAccountService accountService){
_accountService = accountService;
}
}

Injecting dependencies into custom Web API action filter attribute with Autofac

I'm trying to resolve the dependencies of my custom AuthorizeAttribute which I use to decorate my API controllers in an MVC4 app. Problem is that I keep getting a NullReferenceException on the service dependency I use within my custom filter. Here is my Autofac configuration:
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
var builder = new ContainerBuilder();
builder.RegisterApiControllers(Assembly.GetExecutingAssembly());
builder.RegisterType<UnitOfWork>().As<IUnitOfWork>().InstancePerApiRequest();
builder.RegisterType<DatabaseFactory>().As<IDatabaseFactory>().InstancePerApiRequest();
builder.RegisterAssemblyTypes(typeof(UserProfileRepository).Assembly)
.Where(t => t.Name.EndsWith("Repository"))
.AsImplementedInterfaces().InstancePerApiRequest();
builder.RegisterAssemblyTypes(typeof(IUserProfileMapper).Assembly)
.Where(t => t.Name.EndsWith("Mapper"))
.AsImplementedInterfaces().InstancePerApiRequest();
builder.RegisterAssemblyTypes(typeof(UserProfileSvc).Assembly)
.Where(t => t.Name.EndsWith("Svc"))
.AsImplementedInterfaces().InstancePerApiRequest();
builder.RegisterWebApiFilterProvider(config);
var container = builder.Build();
var resolver = new AutofacWebApiDependencyResolver(container);
config.DependencyResolver = resolver;
}
}
and my custom authorize filter:
public class MyAuthorizeAttribute : AuthorizeAttribute
{
public IAuthenticationSvc _authenticationSvc;
protected override bool IsAuthorized(System.Web.Http.Controllers.HttpActionContext actionContext)
{
if (!base.IsAuthorized(actionContext))
{
return false;
}
var trueUserId = WebSecurity.CurrentUserId;
if (_authenticationSvc.GetUsersRoles(trueUserId).Any(x => x == "Admin")) return true;
// NullReferenceException on _authenticationSvc
}
}
According to the official docs all that is needed is:
var builder = new ContainerBuilder();
builder.RegisterWebApiFilterProvider(GlobalConfiguration.Configuration);
But that doesn't seem to do the trick either. Appreciate any help.
I think Autofac's documentation offers much simpler solution for WebApi action filters.
public interface ServiceCallActionFilterAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(HttpActionContext actionContext)
{
// Get the request lifetime scope so you can resolve services.
var requestScope = actionContext.Request.GetDependencyScope();
// Resolve the service you want to use.
var service = requestScope.GetService(typeof(IMyService)) as IMyService;
// Do the rest of the work in the filter.
service.DoWork();
}
}
It is not "pure DI" as it is using service locator, but it is simple and works with the request scope. You don't need to worry about registering specific action filter for each WebApi controller.
Source:
http://autofac.readthedocs.io/en/latest/integration/webapi.html#provide-filters-via-dependency-injection
You should configure property injection for your attribute
public class MyAuthorizeAttribute : AuthorizeAttribute
{
public IAuthenticationSvc AuthenticationSvc { get; set; }
}
and the builder
builder.RegisterType<MyAuthorizeAttribute>().PropertiesAutowired();
In addition to #Toan Nguyen's answer, if you have this...
public class MyAuthorizeAttribute : AuthorizeAttribute
{
public IAuthenticationSvc AuthenticationSvc { get; set; }
}
... it seems you also need (or may need) the first line below:
builder.RegisterFilterProvider();
builder.RegisterType<MyAuthorizeAttribute>().PropertiesAutowired();
Reference: http://itprojectpool.blogspot.com.au/2014/03/autofac-di-on-action-filters.html
In addition to configuring property injection, as outlined in other answers, you can also explicitly resolve dependencies in the OnActivating callback.
public class MyAuthorizeAttribute : AuthorizeAttribute
{
private IAuthenticationSvc _authenticationSvc;
public void SetAuthenticationSvc(IAuthenticationSvc svc)
{
this._authenticationSvc = svc;
}
}
And then register the type:
builder.RegisterType<MyAuthorizeAttribute>()
.OnActivating(_ => _.Instance.SetAuthenticationSvc(_.Context.Resolve<IAuthenticationSvc>()));
Note: You can do the same with a property instead of a method. I chose to use a method here only to illustrate that this solution was not dependent on PropertiesAutowired.

IOC Container use in MVC and a sample project on codeproject

I was wondering if there is a more ellegant pattern for using IOC containers than the way suggested in the codeproject example below.
Surely it is is possible to use an IOC container so that controller constructors are parameterless when you have a solid IOC process in place.
It also means when your application has more than MVC eg WEB-API and whatever else , you are building the same type of solution for that technology.
That doesnt look DRY to me.
Is there a nice pattern someone uses to "register" the Container eg as IMyMagicControllerHat and get singleton using some nice .net System library?
Clearly if you have a UI Depends on Core depends on Model type of application, you are concerned about build dependencies and static calls.
The static calls back to MVCApplication concern me. I was looking for a better approach.
The CODEProject link....
http://www.codeproject.com/Articles/99361/How-To-Use-Unity-Container-In-ASP-NET-MVC-Framewor
In short the relevant code....
public interface IContainerAccessor
{
IUnityContainer Container { get; }
}
public class MvcApplication : HttpApplication, IContainerAccessor
{
private static IUnityContainer _container;
public static IUnityContainer Container
{
get { return _container; }
}
public static IUnityContainer Container
{
get { return _container; }
}
Also there are examples like This from MS that shows a custom static as a solution. http://msdn.microsoft.com/en-us/library/vstudio/hh323691%28v=vs.100%29.aspx I was expecting there might be a well established adapter pattern or similar. Im going to have 100s of accesses to the unity container, so getting the pattern right looks important.
Even blogs that make perfect sense like this, dont show how deep in the application you should get a reference to the original container instance.
http://blog.ploeh.dk/2010/09/29/TheRegisterResolveReleasePattern.aspx
I Appreciate any tips on good ways to solve.
That codeproject article was published in 2010 based on MVC1. MVC3 now contains an IDependencyResolver implementation for better integration with IoC. And for MVC3 with Unity you might look at Unity.MVC3
And if you want to avoid constructor injection you could always register Unity as the default IServiceLocator:
ServiceLocator.SetLocatorProvider(() => new UnityServiceLocator(Container));
In case anyone is interested, during a google search:
I ended using a custom Global Class. 1 Static Call to start things off.
The rest as Interface/adapter pattern. The example register Unity and Enterprise Library.
And records important details from authenticated request for logging.
... this is a partial extract only... For illustration purposes
Call from MVC Application Global.asax
public class BosMasterWebApplication : System.Web.HttpApplication
{
private bool IsBootStraped;
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
WebApiConfig.Register(GlobalConfiguration.Configuration);
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
AuthConfig.RegisterAuth();
IsBootStraped = false;
}
public override void Init()
{
base.Init();
// handlers managed by ASP.Net during Forms authentication
AuthenticateRequest += new EventHandler(AuthHandler);
PostAuthenticateRequest += new EventHandler(PostAuthHandler);
}
public void AuthHandler(object sender, EventArgs e)
{
// if you need to setup anything
}
public void PostAuthHandler(object sender, EventArgs e)
{
// called several times during the authentication process
if (!IsBootStraped)
BosGlobal.HttpBootStrap(this);
if (Request.IsAuthenticated)
{
// did FormsAuthentication.SetAuthCookie was executed if we get here, so set username
BosGlobal.BGA.IBosCurrentState.CurrentUser.SetFromEnvironment();
}
}
}
/// <summary>
/// Central adapter management. Dont abuse this class
/// </summary>
public static class BosGlobal
{
public static IBosGlobalAdpater BGA { get; private set; }
// set up the adapter
static BosGlobal()
{
BGA = new BosGlobalAdapter( );
}
public static void HttpBootStrap(HttpApplication httpApplication)
{
BGA.BootStrap(httpApplication);
}
public static void LocalBootStrap(string machineName)
{ // the LOCAL WPF bootstrap alternative... no http request exists.
BGA.BootStrap(machineName);
}
}
public class BosGlobalAdapter : IBosGlobalAdpater
{
public static bool IsBootStrapped { get; private set; }
/// Unity Container with key dependencies registered
public IUnityContainer IUnityContainer { get; private set; } //IOC Container
//Adapters
public IBosLogAdapter ILogAdapter { get; private set; }
public IBosExceptionManagerAdapter ExceptionManagerAdapter { get; private set; }
public BosGlobalAdapter()
{
IsBootStrapped = false;
IUnityContainer = new UnityContainer();// one container per work process. managing and resolving dependencies
}
public void BootStrap(HttpApplication httpApplication )
{
IBosCurrentState = new BosCurrentState(httpApplication);
BootStrapEnterpriseLibrary();
BootStrapAdapters();
IsBootStrapped = true;
}
private void BootStrapAdapters()
{
ILogAdapter = new BosLogAdapter(IUnityContainer.Resolve<LogWriter>());
ExceptionManagerAdapter = new BosExceptionManagerAdapter(IUnityContainer.Resolve<ExceptionManager>());
}
private void BootStrapEnterpriseLibrary()
{ // now we tell unity about the container manager inside EntLib.
// we dont want 2 containers, so we tell UNity look after EntLib as well please
IUnityContainer.AddNewExtension<EnterpriseLibraryCoreExtension>();
}

How to set ViewBag properties for all Views without using a base class for Controllers?

In the past I've stuck common properties, such as the current user, onto ViewData/ViewBag in a global fashion by having all Controllers inherit from a common base controller.
This allowed my to use IoC on the base controller and not just reach out into global shared for such data.
I'm wondering if there is an alternate way of inserting this kind of code into the MVC pipeline?
The best way is using the ActionFilterAttribute. I'll show you how to use it in .Net Core and .Net Framework.
.Net Core 2.1 & 3.1
public class ViewBagActionFilter : ActionFilterAttribute
{
public ViewBagActionFilter(IOptions<Settings> settings){
//DI will inject what you need here
}
public override void OnResultExecuting(ResultExecutingContext context)
{
// for razor pages
if (context.Controller is PageModel)
{
var controller = context.Controller as PageModel;
controller.ViewData.Add("Avatar", $"~/avatar/empty.png");
// or
controller.ViewBag.Avatar = $"~/avatar/empty.png";
//also you have access to the httpcontext & route in controller.HttpContext & controller.RouteData
}
// for Razor Views
if (context.Controller is Controller)
{
var controller = context.Controller as Controller;
controller.ViewData.Add("Avatar", $"~/avatar/empty.png");
// or
controller.ViewBag.Avatar = $"~/avatar/empty.png";
//also you have access to the httpcontext & route in controller.HttpContext & controller.RouteData
}
base.OnResultExecuting(context);
}
}
Then you need to register this in your startup.cs.
.Net Core 3.1
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews(options => {
options.Filters.Add<Components.ViewBagActionFilter>();
});
}
.Net Core 2.1
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc(options =>
{
options.Filters.Add<Configs.ViewBagActionFilter>();
});
}
Then you can use it in all views and pages
#ViewData["Avatar"]
#ViewBag.Avatar
.Net Framework (ASP.NET MVC .Net Framework)
public class UserProfilePictureActionFilter : ActionFilterAttribute
{
public override void OnResultExecuting(ResultExecutingContext filterContext)
{
filterContext.Controller.ViewBag.IsAuthenticated = MembershipService.IsAuthenticated;
filterContext.Controller.ViewBag.IsAdmin = MembershipService.IsAdmin;
var userProfile = MembershipService.GetCurrentUserProfile();
if (userProfile != null)
{
filterContext.Controller.ViewBag.Avatar = userProfile.Picture;
}
}
}
register your custom class in the global. asax (Application_Start)
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
GlobalFilters.Filters.Add(new UserProfilePictureActionFilter(), 0);
}
Then you can use it in all views
#ViewBag.IsAdmin
#ViewBag.IsAuthenticated
#ViewBag.Avatar
Also there is another way
Creating an extension method on HtmlHelper
[Extension()]
public string MyTest(System.Web.Mvc.HtmlHelper htmlHelper)
{
return "This is a test";
}
Then you can use it in all views
#Html.MyTest()
Since ViewBag properties are, by definition, tied to the view presentation and any light view logic that may be necessary, I'd create a base WebViewPage and set the properties on page initialization. It's very similar to the concept of a base controller for repeated logic and common functionality, but for your views:
public abstract class ApplicationViewPage<T> : WebViewPage<T>
{
protected override void InitializePage()
{
SetViewBagDefaultProperties();
base.InitializePage();
}
private void SetViewBagDefaultProperties()
{
ViewBag.GlobalProperty = "MyValue";
}
}
And then in \Views\Web.config, set the pageBaseType property:
<system.web.webPages.razor>
<host factoryType="System.Web.Mvc.MvcWebRazorHostFactory, System.Web.Mvc, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
<pages pageBaseType="MyNamespace.ApplicationViewPage">
<namespaces>
<add namespace="System.Web.Mvc" />
<add namespace="System.Web.Mvc.Ajax" />
<add namespace="System.Web.Mvc.Html" />
<add namespace="System.Web.Routing" />
</namespaces>
</pages>
</system.web.webPages.razor>
Un-tried by me, but you might look at registering your views and then setting the view data during the activation process.
Because views are registered on-the-fly, the registration syntax doesn't help you with connecting to the Activated event, so you'd need to set it up in a Module:
class SetViewBagItemsModule : Module
{
protected override void AttachToComponentRegistration(
IComponentRegistration registration,
IComponentRegistry registry)
{
if (typeof(WebViewPage).IsAssignableFrom(registration.Activator.LimitType))
{
registration.Activated += (s, e) => {
((WebViewPage)e.Instance).ViewBag.Global = "global";
};
}
}
}
This might be one of those "only tool's a hammer"-type suggestions from me; there may be simpler MVC-enabled ways to get at it.
Edit: Alternate, less code approach - just attach to the Controller
public class SetViewBagItemsModule: Module
{
protected override void AttachToComponentRegistration(IComponentRegistry cr,
IComponentRegistration reg)
{
Type limitType = reg.Activator.LimitType;
if (typeof(Controller).IsAssignableFrom(limitType))
{
registration.Activated += (s, e) =>
{
dynamic viewBag = ((Controller)e.Instance).ViewBag;
viewBag.Config = e.Context.Resolve<Config>();
viewBag.Identity = e.Context.Resolve<IIdentity>();
};
}
}
}
Edit 2: Another approach that works directly from the controller registration code:
builder.RegisterControllers(asm)
.OnActivated(e => {
dynamic viewBag = ((Controller)e.Instance).ViewBag;
viewBag.Config = e.Context.Resolve<Config>();
viewBag.Identity = e.Context.Resolve<IIdentity>();
});
Brandon's post is right on the money. As a matter of fact, I would take this a step further and say that you should just add your common objects as properties of the base WebViewPage so you don't have to cast items from the ViewBag in every single View. I do my CurrentUser setup this way.
You could use a custom ActionResult:
public class GlobalView : ActionResult
{
public override void ExecuteResult(ControllerContext context)
{
context.Controller.ViewData["Global"] = "global";
}
}
Or even a ActionFilter:
public class GlobalView : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
filterContext.Result = new ViewResult() {ViewData = new ViewDataDictionary()};
base.OnActionExecuting(filterContext);
}
}
Had an MVC 2 project open but both techniques still apply with minor changes.
You don't have to mess with actions or change the model, just use a base controller and cast the existing controller from the layout viewcontext.
Create a base controller with the desired common data (title/page/location etc) and action initialization...
public abstract class _BaseController:Controller {
public Int32 MyCommonValue { get; private set; }
protected override void OnActionExecuting(ActionExecutingContext filterContext) {
MyCommonValue = 12345;
base.OnActionExecuting(filterContext);
}
}
Make sure every controller uses the base controller...
public class UserController:_BaseController {...
Cast the existing base controller from the view context in your _Layout.cshml page...
#{
var myController = (_BaseController)ViewContext.Controller;
}
Now you can refer to values in your base controller from your layout page.
#myController.MyCommonValue
If you want compile time checking and intellisense for the properties in your views then the ViewBag isn't the way to go.
Consider a BaseViewModel class and have your other view models inherit from this class, eg:
Base ViewModel
public class BaseViewModel
{
public bool IsAdmin { get; set; }
public BaseViewModel(IUserService userService)
{
IsAdmin = userService.IsAdmin;
}
}
View specific ViewModel
public class WidgetViewModel : BaseViewModel
{
public string WidgetName { get; set;}
}
Now view code can access the property directly in the view
<p>Is Admin: #Model.IsAdmin</p>
I have found the following approach to be the most efficient and gives excellent control utilizing the _ViewStart.chtml file and conditional statements when necessary:
_ViewStart:
#{
Layout = "~/Views/Shared/_Layout.cshtml";
var CurrentView = ViewContext.Controller.ValueProvider.GetValue("controller").RawValue.ToString();
if (CurrentView == "ViewA" || CurrentView == "ViewB" || CurrentView == "ViewC")
{
PageData["Profile"] = db.GetUserAccessProfile();
}
}
ViewA:
#{
var UserProfile= PageData["Profile"] as List<string>;
}
Note:
PageData will work perfectly in Views; however, in the case of a
PartialView, it will need to be passed from the View to
the child Partial.
I implemented the ActionFilterAttribute solution from #Mohammad Karimi. It worked well as I had the same scenario as the OP. I needed to add data to every view. The action filter attribute was executed for every Razor page request, but it was also called for every web API controller request.
Razor Pages offers a page filter attribute to avoid unnecessary execution of the action filter when a web API controller request is made.
Razor Page filters IPageFilter and IAsyncPageFilter allow Razor Pages to run code before and after a Razor Page handler is run.
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Configuration;
namespace MyProject
{
// learn.microsoft.com/en-us/aspnet/core/razor-pages/filter?view=aspnetcore-6.0
// "The following code implements the synchronous IPageFilter"
// Enable the page filter using 'services.AddRazorPages().AddMvcOptions( ... )
// in the 'ConfigureServices()' startup method.
public class ViewDataPageFilter : IPageFilter
{
private readonly IConfiguration _config;
public ViewDataPageFilter(IConfiguration config)
{
_config = config;
}
// "Called after a handler method has been selected,
// but before model binding occurs."
public void OnPageHandlerSelected(PageHandlerSelectedContext context)
{
}
// "Called before the handler method executes,
// after model binding is complete."
public void OnPageHandlerExecuting(PageHandlerExecutingContext context)
{
PageModel page = context.HandlerInstance as PageModel;
if (page == null) { return; }
page.ViewData["cdn"] = _config["cdn:url"];
}
// "Called after the handler method executes,
// before the action result."
public void OnPageHandlerExecuted(PageHandlerExecutedContext context)
{
}
}
}
As per the sample in the filter methods for Razor Pages documentation, the page filter is enabled by:
public void ConfigureServices(IServiceCollection services)
{
services.AddRazorPages()
.AddMvcOptions(options =>
{
options.Filters.Add(new ViewDataPageFilter(Configuration));
});
}

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