I really like Caliburn and the naming convention binding and was surprised that
the Visibility is not bound in the same way the "CanNAME" Convention is used to guard an Action.
As far as I know is the BooleanToVisibilityConverter only used when Binding is explicitly used in Caliburn and not automatically like the guard method. So I was thinking to modify the source to bind automatically to "bool? ControlNameIsVisible()" (null equals collapse) or similar. I was wondering if that is the right approach and if soo if somebody has done the implementation already and could share it here.
You could use this approach if you wanted, it's perfectly reasonable. Another approach is to use a Border with the same name as a boolean property on your view model. Caliburn.Micro will set the visibility of the Border based on the value of the boolean property.
<Border x:Name="ControlIsVisible">
<TextBox x:Name="MyControl" ... />
</Border>
If you want a general solution, this is what I ended up with, based on: Adding a convention for IsEnabled to Caliburn.Micro
Note the overriding of BindActions as well as BindProperties so that you can check for Visibility on things that have actions bound to them.
protected override void Configure()
{
base.Configure();
ConventionManager.AddElementConvention<UIElement>(UIElement.VisibilityProperty, "Visibility", "VisibilityChanged");
var baseBindProperties = ViewModelBinder.BindProperties;
ViewModelBinder.BindProperties =
(frameWorkElements, viewModel) =>
{
BindVisiblityProperties(frameWorkElements, viewModel);
return baseBindProperties(frameWorkElements, viewModel);
};
// Need to override BindActions as well, as it's called first and filters out anything it binds to before
// BindProperties is called.
var baseBindActions = ViewModelBinder.BindActions;
ViewModelBinder.BindActions =
(frameWorkElements, viewModel) =>
{
BindVisiblityProperties(frameWorkElements, viewModel);
return baseBindActions(frameWorkElements, viewModel);
};
}
void BindVisiblityProperties(IEnumerable<FrameworkElement> frameWorkElements, Type viewModel)
{
foreach (var frameworkElement in frameWorkElements)
{
var propertyName = frameworkElement.Name + "IsVisible";
var property = viewModel.GetPropertyCaseInsensitive(propertyName);
if (property != null)
{
var convention = ConventionManager
.GetElementConvention(typeof(FrameworkElement));
ConventionManager.SetBindingWithoutBindingOverwrite(
viewModel,
propertyName,
property,
frameworkElement,
convention,
convention.GetBindableProperty(frameworkElement));
}
}
}
Related
Is it ok to have simple logic (without any dependencies) in ViewModels getters or it should contain just automatic properties? in this case just checking for null so I don't have to do that in controller each time I am using this ViewModel. TicketSearchParameters is a simple model containing string and date properties, there is no Repository or any other dependencies.
public class MyViewModel
{
private TicketSearchParameters _searchParams;
public TicketSearchParameters SearchParams
{
get
{
if (_searchParams == null)
{
_searchParams = new TicketSearchParameters();
_searchParams.CreatedFrom = DateTime.Now.AddDays(-7);
_searchParams.CreatedTo = DateTime.Now;
}
return _searchParams;
}
set
{
_searchParams = value;
}
}
/*** other properties ***/
}
You code is fairly ok. But you can use NULL Object Design Pattern to check null and create NullObject.
make a class named NullSearchParams inherited from SearchParams and initialize it when needed.
You can see Null design pattern documentation here.
https://sourcemaking.com/design_patterns/null_object
Is there a way to make a reflection for a view model property as an element with different name and id values on the html side.
That is the main question of what I want to achieve. So the basic introduction for the question is like:
1- I have a view model (as an example) which created for a filter operation in view side.
public class FilterViewModel
{
public string FilterParameter { get; set; }
}
2- I have a controller action which is created for GETting form values(here it is filter)
public ActionResult Index(FilterViewModel filter)
{
return View();
}
3- I have a view that a user can filter on some data and sends parameters via querystring over form submit.
#using (Html.BeginForm("Index", "Demo", FormMethod.Get))
{
#Html.LabelFor(model => model.FilterParameter)
#Html.EditorFor(model => model.FilterParameter)
<input type="submit" value="Do Filter" />
}
4- And what I want to see in rendered view output is
<form action="/Demo" method="get">
<label for="fp">FilterParameter</label>
<input id="fp" name="fp" type="text" />
<input type="submit" value="Do Filter" />
</form>
5- And as a solution I want to modify my view model like this:
public class FilterViewModel
{
[BindParameter("fp")]
[BindParameter("filter")] // this one extra alias
[BindParameter("param")] //this one extra alias
public string FilterParameter { get; set; }
}
So the basic question is about BindAttribute but the usage of complex type properties. But also if there is a built in way of doing this is much better.
Built-in pros:
1- Usage with TextBoxFor, EditorFor, LabelFor and other strongly typed view model helpers can understand and communicate better with each other.
2- Url routing support
3- No framework by desing problems :
In general, we recommend folks don’t write custom model binders
because they’re difficult to get right and they’re rarely needed. The
issue I’m discussing in this post might be one of those cases where
it’s warranted.
Link of quote
And also after some research I found these useful works:
Binding model property with different name
One step upgrade of first link
Here some informative guide
Result: But none of them give me my problems exact solution. I am looking for a strongly typed solution for this problem. Of course if you know any other way to go, please share.
Update
The underlying reasons why I want to do this are basically:
1- Everytime I want to change the html control name then I have to change PropertyName at compile time. (There is a difference Changing a property name between changing a string in code)
2- I want to hide (camouflage) real property names from end users. Most of times View Model property names same as mapped Entity Objects property names. (For developer readability reasons)
3- I don't want to remove the readability for developer. Think about lots of properties with like 2-3 character long and with mo meanings.
4- There are lots of view models written. So changing their names are going to take more time than this solution.
5- This is going to be better solution (in my POV) than others which are described in other questions until now.
Actually, there is a way to do it.
In ASP.NET binding metadata gathered by TypeDescriptor, not by reflection directly. To be more precious, AssociatedMetadataTypeTypeDescriptionProvider is used, which, in turn, simply calls TypeDescriptor.GetProvider with our model type as parameter:
public AssociatedMetadataTypeTypeDescriptionProvider(Type type)
: base(TypeDescriptor.GetProvider(type))
{
}
So, everything we need is to set our custom TypeDescriptionProvider for our model.
Let's implement our custom provider. First of all, let's define attribute for custom property name:
[AttributeUsage(AttributeTargets.Property)]
public class CustomBindingNameAttribute : Attribute
{
public CustomBindingNameAttribute(string propertyName)
{
this.PropertyName = propertyName;
}
public string PropertyName { get; private set; }
}
If you already have attribute with desired name, you can reuse it. Attribute defined above is just an example. I prefer to use JsonPropertyAttribute because in most cases I work with json and Newtonsoft's library and want to define custom name only once.
The next step is to define custom type descriptor. We will not implement whole type descriptor logic and use default implementation. Only property accessing will be overridden:
public class MyTypeDescription : CustomTypeDescriptor
{
public MyTypeDescription(ICustomTypeDescriptor parent)
: base(parent)
{
}
public override PropertyDescriptorCollection GetProperties()
{
return Wrap(base.GetProperties());
}
public override PropertyDescriptorCollection GetProperties(Attribute[] attributes)
{
return Wrap(base.GetProperties(attributes));
}
private static PropertyDescriptorCollection Wrap(PropertyDescriptorCollection src)
{
var wrapped = src.Cast<PropertyDescriptor>()
.Select(pd => (PropertyDescriptor)new MyPropertyDescriptor(pd))
.ToArray();
return new PropertyDescriptorCollection(wrapped);
}
}
Also custom property descriptor need to be implemented. Again, everything except property name will be handled by default descriptor. Note, NameHashCode for some reason is a separate property. As name changed, so it's hash code need to be changed too:
public class MyPropertyDescriptor : PropertyDescriptor
{
private readonly PropertyDescriptor _descr;
private readonly string _name;
public MyPropertyDescriptor(PropertyDescriptor descr)
: base(descr)
{
this._descr = descr;
var customBindingName = this._descr.Attributes[typeof(CustomBindingNameAttribute)] as CustomBindingNameAttribute;
this._name = customBindingName != null ? customBindingName.PropertyName : this._descr.Name;
}
public override string Name
{
get { return this._name; }
}
protected override int NameHashCode
{
get { return this.Name.GetHashCode(); }
}
public override bool CanResetValue(object component)
{
return this._descr.CanResetValue(component);
}
public override object GetValue(object component)
{
return this._descr.GetValue(component);
}
public override void ResetValue(object component)
{
this._descr.ResetValue(component);
}
public override void SetValue(object component, object value)
{
this._descr.SetValue(component, value);
}
public override bool ShouldSerializeValue(object component)
{
return this._descr.ShouldSerializeValue(component);
}
public override Type ComponentType
{
get { return this._descr.ComponentType; }
}
public override bool IsReadOnly
{
get { return this._descr.IsReadOnly; }
}
public override Type PropertyType
{
get { return this._descr.PropertyType; }
}
}
Finally, we need our custom TypeDescriptionProvider and way to bind it to our model type. By default, TypeDescriptionProviderAttribute is designed to perform that binding. But in this case we will not able to get default provider that we want to use internally. In most cases, default provider will be ReflectTypeDescriptionProvider. But this is not guaranteed and this provider is inaccessible due to it's protection level - it's internal. But we do still want to fallback to default provider.
TypeDescriptor also allow to explicitly add provider for our type via AddProvider method. That what we will use. But firstly, let's define our custom provider itself:
public class MyTypeDescriptionProvider : TypeDescriptionProvider
{
private readonly TypeDescriptionProvider _defaultProvider;
public MyTypeDescriptionProvider(TypeDescriptionProvider defaultProvider)
{
this._defaultProvider = defaultProvider;
}
public override ICustomTypeDescriptor GetTypeDescriptor(Type objectType, object instance)
{
return new MyTypeDescription(this._defaultProvider.GetTypeDescriptor(objectType, instance));
}
}
The last step is to bind our provider to our model types. We can implement it in any way we want. For example, let's define some simple class, such as:
public static class TypeDescriptorsConfig
{
public static void InitializeCustomTypeDescriptorProvider()
{
// Assume, this code and all models are in one assembly
var types = Assembly.GetExecutingAssembly().GetTypes()
.Where(t => t.GetProperties().Any(p => p.IsDefined(typeof(CustomBindingNameAttribute))));
foreach (var type in types)
{
var defaultProvider = TypeDescriptor.GetProvider(type);
TypeDescriptor.AddProvider(new MyTypeDescriptionProvider(defaultProvider), type);
}
}
}
And either invoke that code via web activation:
[assembly: PreApplicationStartMethod(typeof(TypeDescriptorsConfig), "InitializeCustomTypeDescriptorProvider")]
Or simply call it in Application_Start method:
public class MvcApplication : HttpApplication
{
protected void Application_Start()
{
TypeDescriptorsConfig.InitializeCustomTypeDescriptorProvider();
// rest of init code ...
}
}
But this is not the end of the story. :(
Consider following model:
public class TestModel
{
[CustomBindingName("actual_name")]
[DisplayName("Yay!")]
public string TestProperty { get; set; }
}
If we try to write in .cshtml view something like:
#model Some.Namespace.TestModel
#Html.DisplayNameFor(x => x.TestProperty) #* fail *#
We will get ArgumentException:
An exception of type 'System.ArgumentException' occurred in System.Web.Mvc.dll but was not handled in user code
Additional information: The property Some.Namespace.TestModel.TestProperty could not be found.
That because all helpers soon or later invoke ModelMetadata.FromLambdaExpression method. And this method take expression we provided (x => x.TestProperty) and takes member name directly from member info and have no clue about any of our attributes, metadata (who cares, huh?):
internal static ModelMetadata FromLambdaExpression<TParameter, TValue>(/* ... */)
{
// ...
case ExpressionType.MemberAccess:
MemberExpression memberExpression = (MemberExpression) expression.Body;
propertyName = memberExpression.Member is PropertyInfo ? memberExpression.Member.Name : (string) null;
// I want to cry here - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// ...
}
For x => x.TestProperty (where x is TestModel) this method will return TestProperty, not actual_name, but model metadata contains actual_name property, have no TestProperty. That is why the property could not be found error thrown.
This is a design failure.
However despite this little inconvenience there are several workarounds, such as:
The easiest way is to access our members by theirs redefined names:
#model Some.Namespace.TestModel
#Html.DisplayName("actual_name") #* this will render "Yay!" *#
This is not good. No intellisense at all and as our model change we will have no any compilation errors. On any change anything can be broken and there is no easy way to detect that.
Another way is a bit more complex - we can create our own version of that helpers and forbid anybody from calling default helpers or ModelMetadata.FromLambdaExpression for model classes with renamed properties.
Finally, combination of previous two would be preferred: write own analogue to get property name with redefinition support, then pass that into default helper. Something like this:
#model Some.Namespace.TestModel
#Html.DisplayName(Html.For(x => x.TestProperty))
Compilation-time and intellisense support and no need to spend a lot of time for complete set of helpers. Profit!
Also everything described above work like a charm for model binding. During model binding process default binder also use metadata, gathered by TypeDescriptor.
But I guess binding json data is the best use case. You know, lots of web software and standards use lowercase_separated_by_underscores naming convention. Unfortunately this is not usual convention for C#. Having classes with members named in different convention looks ugly and can end up in troubles. Especially when you have tools that whining every time about naming violation.
ASP.NET MVC default model binder does not bind json to model the same way as it happens when you call newtonsoft's JsonConverter.DeserializeObject method. Instead, json parsed into dictionary. For example:
{
complex: {
text: "blabla",
value: 12.34
},
num: 1
}
will be translated into following dictionary:
{ "complex.text", "blabla" }
{ "complex.value", "12.34" }
{ "num", "1" }
And later these values along with others values from query string, route data and so on, collected by different implementations of IValueProvider, will be used by default binder to bind a model with help of metadata, gathered by TypeDescriptor.
So we came full circle from creating model, rendering, binding it back and use it.
The short answer is NO and long answer still NO. There is no built-in helper, attribute, model binder, whatever is it (Nothing out of box).
But what I did in before answer (I deleted it) was an awful solution that I realized yesterday. I am going to put it in github for who still wants to see (maybe it solves somebody problem) (I don't suggest it also!)
Now I searched it for again and I couldn't find anything helpful. If you are using something like AutoMapper or ValueInjecter like tool for mapping your ViewModel objects to Business objects and if you want to obfuscate that View Model parameters also, probably you are in some trouble. Of course you can do it but strongly typed html helpers are not going to help you alot. I even not talking about the if other developers taking branch and working over common view models.
Luckily my project (4 people working on it, and its commercial use for) not that big for now, so I decided to change View Model property names! (It is still lot work to do. Hundreds of view models to obfuscate their properties!!!) Thank you Asp.Net MVC !
There some ways in the links which I gave in question. But also if you still want to use the BindAlias attribute, I can only suggest you to use the following extension methods. At least you dont have to write same alias string which you write in BindAlias attribute.
Here it is:
public static string AliasNameFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper,
Expression<Func<TModel, TProperty>> expression)
{
var memberExpression = ExpressionHelpers.GetMemberExpression(expression);
if (memberExpression == null)
throw new InvalidOperationException("Expression must be a member expression");
var aliasAttr = memberExpression.Member.GetAttribute<BindAliasAttribute>();
if (aliasAttr != null)
{
return MvcHtmlString.Create(aliasAttr.Alias).ToHtmlString();
}
return htmlHelper.NameFor(expression).ToHtmlString();
}
public static string AliasIdFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper,
Expression<Func<TModel, TProperty>> expression)
{
var memberExpression = ExpressionHelpers.GetMemberExpression(expression);
if (memberExpression == null)
throw new InvalidOperationException("Expression must be a member expression");
var aliasAttr = memberExpression.Member.GetAttribute<BindAliasAttribute>();
if (aliasAttr != null)
{
return MvcHtmlString.Create(TagBuilder.CreateSanitizedId(aliasAttr.Alias)).ToHtmlString();
}
return htmlHelper.IdFor(expression).ToHtmlString();
}
public static T GetAttribute<T>(this ICustomAttributeProvider provider)
where T : Attribute
{
var attributes = provider.GetCustomAttributes(typeof(T), true);
return attributes.Length > 0 ? attributes[0] as T : null;
}
public static MemberExpression GetMemberExpression<TModel, TProperty>(Expression<Func<TModel, TProperty>> expression)
{
MemberExpression memberExpression;
if (expression.Body is UnaryExpression)
{
var unaryExpression = (UnaryExpression)expression.Body;
memberExpression = (MemberExpression)unaryExpression.Operand;
}
else
{
memberExpression = (MemberExpression)expression.Body;
}
return memberExpression;
}
When you want to use it:
[ModelBinder(typeof(AliasModelBinder))]
public class FilterViewModel
{
[BindAlias("someText")]
public string FilterParameter { get; set; }
}
In html:
#* at least you dont write "someText" here again *#
#Html.Editor(Html.AliasNameFor(model => model.FilterParameter))
#Html.ValidationMessage(Html.AliasNameFor(model => model.FilterParameter))
So I am leaving this answer here like this. This is even not an answer (and there is no answer for MVC 5) but who searching in google for same problem might find useful this experience.
And here is the github repo: https://github.com/yusufuzun/so-view-model-bind-20869735
I have a viewmodel which is returned from a insert call on a MVC page.
It exposes properties according to the underlying model as well as some calculated properties.
Everything is fine except when I run an if (tryupdateModel(viewModel)). At which point it seems the calculated properties cause an error and it wont pass the if statement?
Is there an annotation I can put on this property to prevent it being checked in the tryupdate?
or how do i determine exactly what it is thats not allowing this to return true?
Is there an annotation I can put on
this property to prevent it being
checked in the tryupdate?
One way to prohibit updating a particular property is to use:
TryUpdateModel(model, null, null, new [] {"SecretProperty"}); // Blacklist
TryUpdateModel(model, new [] {"Prop1", "Prop2", "etc"}); // Whitelist - recommended
The other is to implement custom ModelBinder that reflects on the properties and ignores ones that should not be updated:
public class ApplicationModelBinder : DefaultModelBinder {
protected override PropertyDescriptorCollection GetModelProperties(ControllerContext controllerContext, ModelBindingContext bindingContext) {
var allProps = GetTypeDescriptor(controllerContext, bindingContext).GetProperties();
var resulting = new PropertyDescriptorCollection(null);
//Filter out the props with no scaffolding set
foreach(PropertyDescriptor prop in allProps) {
if (ShouldIncludeProperty(bindingContext, prop))
resulting.Add(prop);
}
return resulting;
}
private static bool ShouldIncludeProperty(ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor) {
var doNotScaffold = propertyDescriptor.Attributes.OfType<ScaffoldColumnAttribute>().Any(x => !x.Scaffold);
return !doNotScaffold;
}
}
Then you can apply [ScaffoldColumn(false)] to the property that is not updateable.
I use this approach.
how do i determine exactly what it is thats not allowing this to return true?
After the call to TryUpdateModel you can inspect ModelState property of the controller and find where the errors are.
This will depend on how you are performing validation. If you are using data annotations you need to remove any attributes on the calculated properties. You also need to check which property is failing validation. You may execute the following command after TryUpdateModel:
var errors = ModelState
.Where(x => x.Value.Errors.Count > 0)
.Select(x => new { x.Key, x.Value.Errors })
.ToArray();
This will give you the list of errors and their corresponding properties and might help you understand what is happening and why validation is failing.
I have a BaseViewModel that my View Models all inherit from.
public class MagazineViewModel : BaseOutputViewMode
{
public string TitleOfPublication { get; set; }
}
In my controller I use a factory method to give the corret View Model back based on an input:
// e.g. viewModel contains an instance of MagazineViewModel
BaseOutputViewModel viewModel = BaseOutputViewModel.GetOutputViewModel(output);
When I use TryUpdateModel to try and bind to a FormCollection which I know contains a "TitleOfPublication" key, its never set in my view model:
if (!TryUpdateModel(viewModel, form))
I think this is something to do with the DefaultModelBinder using the BaseOutputViewModel to bind FormCollection keys to - it doesn't contain a "TitleOfPublication", the derived MagazineViewModel does.
I'm trying to roll my own model binder, to override the DefaultModelBinder's BindModel behavior. Its all wired in correctly and I can debug into it straight after the TryUpdateModel call:
public class TestModelBinder : DefaultModelBinder, IFilteredModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
// Tried the following without success ....
// 1. Quick hardcoded test
// bindingContext.ModelType = typeof(MagazineViewModel);
// 2. Set ModelMetadata, hardcoded test again
// bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, typeof(MagazineViewModel));
// 3. Replace the entire context
// ModelBindingContext context2 = new ModelBindingContext();
// context2.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, typeof(MagazineViewModel));
// context2.ModelName = bindingContext.ModelName;
// context2.ModelState = bindingContext.ModelState;
// context2.ValueProvider = bindingContext.ValueProvider;
// bindingContext = context2;
}
}
But I'm not sure how to work with the bindingContext? What needs to be updated so that I can tell the DefaultModelBinder to bind using the derived View Model properties?
Or have I just totally mis-understood this!
I did try overriding CreateModel - much like the DerivedTypeModelBinder in MvcContrib, but I think because I'm giving the binder an instance of a model to work with, CreateModel is never called. Used Reflector on the Mvc DLL, theres a "BindComplexModel" that calls CreateModel only if the model is null:
if (model == null)
{
model = this.CreateModel(controllerContext, bindingContext, modelType);
}
Any pointers greatfully received!
Cheers
OK - finally got to the bottom of this!
In actual fact there was nothing wrong with my model binder, the problem ultimately led back to a couple of input tags that had no name/id:
<input id="" name="" type="text">
The crux was this test in DefaultModelBinder:
// Simple model = int, string, etc.; determined by calling TypeConverter.CanConvertFrom(typeof(string))
// or by seeing if a value in the request exactly matches the name of the model we're binding.
// Complex type = everything else.
if (!performedFallback) {
ValueProviderResult vpResult =
bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (vpResult != null) {
return BindSimpleModel(controllerContext, bindingContext, vpResult);
}
}
With no id/name, the form collection has a key of "" which means that the GetValue correctly returned the value for that field, continuing to bind as a simple model.
When an id/name are added, the form collection contains no key of "", (which is now the name of my model as we're using TryUpdateModel). This meant the DefaultModelBinder correctly treated my model as complexm successfully binding properties in my derived type!
Cheers
My current problem is that I have a partial view that I want to determine what model is being used by it.
I have had to deal with a few strange scenarios for my project so I will try to outline it here, maybe someone can offer a better way to do this.
I am designing something like the Google iGoogle page. A main page with multiple widgets that are able to move around or be configured as needed. The current system loads the actual widget's data asynchronously view a POST to a controller within my application. That controller will either render a partial view to HTML that can be returned (and then loaded into the page view JQUERY) or just straight HTML/JavaScript that is stored in a database.
This was working fine for me, I had a model for the widgets that holds a dictionary of options that are described via the database, and then used by the partial view. The problem came when I wanted to pass data to a partial view. The best solution I could come up with was having the controller determine which model the partial view in question uses, have some function that will fill the model, and then pass it, along with the partial view, to the function that will render it to HTML within the controller.
I realize this is an odd scenario for MVC (the layers are blending...) and any advice on fundamental design, or implementation of this would be greatly appreciated.
I am currently using MVC3/Razor. Feel free to ask any other questions.
I prototyped a possible solution to this, because it seemed like a fun problem. I hope it's useful to you.
Models
First, the models. I decided to create two 'widgets', one for news, and one for a clock.
public class NewsModel
{
public string[] Headlines { get; set; }
public NewsModel(params string[] headlines)
{
Headlines = headlines;
}
}
public class ClockModel
{
public DateTime Now { get; set; }
public ClockModel(DateTime now)
{
Now = now;
}
}
Controller
My controller doesn't know anything about the views. What it does is returns a single model, but that model has the ability to dynamically fetch the right model as required by the view.
public ActionResult Show(string widgetName)
{
var selector = new ModelSelector();
selector.WhenRendering<ClockModel>(() => new ClockModel(DateTime.Now));
selector.WhenRendering<NewsModel>(() => new NewsModel("Headline 1", "Headline 2", "Headline 3"));
return PartialView(widgetName, selector);
}
Delegates are used so that the correct model is only created/fetched if it is actually used.
ModelSelector
The ModelSelector that the controller uses is pretty simple - it just keeps a bag of delegates to create each model type:
public class ModelSelector
{
private readonly Dictionary<Type, Func<object>> modelLookup = new Dictionary<Type, Func<object>>();
public void WhenRendering<T>(Func<object> getter)
{
modelLookup.Add(typeof(T), getter);
}
public object GetModel(Type modelType)
{
if (!modelLookup.ContainsKey(modelType))
{
throw new KeyNotFoundException(string.Format("A provider for the model type '{0}' was not provided", modelType.FullName));
}
return modelLookup[modelType]();
}
}
The Views - Simple solution
Now, the easiest way to implement a view would be:
#model MvcApplication2.ModelSelector
#using MvcApplication2.Models
#{
var clock = (ClockModel) Model.GetModel(typeof (ClockModel));
}
<h2>The time is: #clock.Now</h2>
You could end here and use this approach.
The Views - Better solution
That's pretty ugly. I wanted my views to look like this:
#model MvcApplication2.Models.ClockModel
<h2>Clock</h2>
#Model.Now
And
#model MvcApplication2.Models.NewsModel
<h2>News Widget</h2>
#foreach (var headline in Model.Headlines)
{
<h3>#headline</h3>
}
To make this work, I had to create a custom view engine.
Custom view engine
When a Razor view is compiled, it inherits a ViewPage<T>, where T is the #model. So we can use reflection to figure out what type the view wanted, and select it.
public class ModelSelectorEnabledRazorViewEngine : RazorViewEngine
{
protected override IView CreateView(ControllerContext controllerContext, string viewPath, string masterPath)
{
var result = base.CreateView(controllerContext, viewPath, masterPath);
if (result == null)
return null;
return new CustomRazorView((RazorView) result);
}
protected override IView CreatePartialView(ControllerContext controllerContext, string partialPath)
{
var result = base.CreatePartialView(controllerContext, partialPath);
if (result == null)
return null;
return new CustomRazorView((RazorView)result);
}
public class CustomRazorView : IView
{
private readonly RazorView view;
public CustomRazorView(RazorView view)
{
this.view = view;
}
public void Render(ViewContext viewContext, TextWriter writer)
{
var modelSelector = viewContext.ViewData.Model as ModelSelector;
if (modelSelector == null)
{
// This is not a widget, so fall back to stock-standard MVC/Razor rendering
view.Render(viewContext, writer);
return;
}
// We need to work out what #model is on the view, so that we can pass the correct model to it.
// We can do this by using reflection over the compiled views, since Razor views implement a
// ViewPage<T>, where T is the #model value.
var compiledViewType = BuildManager.GetCompiledType(view.ViewPath);
var baseType = compiledViewType.BaseType;
if (baseType == null || !baseType.IsGenericType)
{
throw new Exception(string.Format("When the view '{0}' was compiled, the resulting type was '{1}', with base type '{2}'. I expected a base type with a single generic argument; I don't know how to handle this type.", view.ViewPath, compiledViewType, baseType));
}
// This will be the value of #model
var modelType = baseType.GetGenericArguments()[0];
if (modelType == typeof(object))
{
// When no #model is set, the result is a ViewPage<object>
throw new Exception(string.Format("The view '{0}' needs to include the #model directive to specify the model type. Did you forget to include an #model line?", view.ViewPath));
}
var model = modelSelector.GetModel(modelType);
// Switch the current model from the ModelSelector to the value of #model
viewContext.ViewData.Model = model;
view.Render(viewContext, writer);
}
}
}
The view engine is registered by putting this in Global.asax.cs:
ViewEngines.Engines.Clear();
ViewEngines.Engines.Add(new ModelSelectorEnabledRazorViewEngine());
Rendering
My home view includes the following lines to test it all out:
#Html.Action("Show", "Widget", new { widgetName = "Clock" })
#Html.Action("Show", "Widget", new { widgetName = "News" })
One option would be to extend the idea of partial requests in your application. Steve Sanderson has a fantastic example of this, although the post relates to MVC 1 & 2. I think it would still help in you v3, but I haven't investigated v3 to see if the MVC team implemented their own version. In your asynch scenario, you'll need to toy with the implementation a bit, perhaps change the PartialRequest definition to accept different information as needed, but I think this might be a good start. The net result would be better isolation of concerns, allowing individual controllers to manage a particular type of partial, and in turn be better aware of the model Type you want to work with.
I'm not 100% sure that this is what you'd be looking for, but the [ChildActionOnly] attribute can be added to a method within your controller. That requires that the method can only be called from a partial view. Then you can set up your partial view for that method that basically resembles one of your widgets. Check out the MVC Music Store example here:
http://www.asp.net/mvc/tutorials/mvc-music-store-part-10
What about a dynamic view model? Layouts in MVC3 use them, and maybe you can use something similar for your purposes:
Dynamic in C# 4.0: Introducing the ExpandoObject
Fun With Method Missing and C# 4
Dynamic View Page, MVC without a View Model
I blogged about doing exactly this. Please see http://blogs.planetcloud.co.uk/mygreatdiscovery/?tag=/widget
Essentially I built out a similar widget system. The posts also cover how to handle configuration of those widgets. This makes use of the dynamic support in Mvc3 so that any model object can be passed to the view, from a single controller action.
By default all widgets have a collection of KVP properties (I believe this is what the OP has). So for a simple widget we get access to those properties from within the view. I used for a widget that displayed some html (where the html was stored in one of those properties).
However, for more complex widgets we implement IWidgetWithDisplayModel. This tells us that before we pass the loaded widget back to the view, we need to "build" our display model.
Here's the controller action that does that. Check the posts for full details.
[HttpGet]
public ActionResult Get(string name)
{
var widget = widgetService.GetWidgetBySystemName(name, true);
if (widget == null)
return Content(string.Format("Widget [{0}] not found!", name));
if (!this.ViewExists(widget.WidgetName))
return Content(string.Format("A template for widget [{0}] was not found.", widget.WidgetName));
if (widget is IWidgetWithDisplayModel) {
(widget as IWidgetWithDisplayModel).CreateDisplayModel();
}
return PartialView(widget.WidgetName, widget);
}