I have the following type hierarchy for ClientIndexModel:
public class ViewModel
{
public virtual IDictionary<string, SelectList> SelectListDictionary
{
get
{
var props = GetType().GetProperties().Where(p => p.PropertyType == typeof(SelectList));
return props.ToDictionary(prop => prop.Name, prop => (SelectList)prop.GetValue(this, null));
}
}
}
public class IndexModel<TIndexItem, TEntity> : ViewModel where TIndexItem : ViewModel where TEntity : new()
{
public List<TIndexItem> Items { get; private set; }
}
public class ClientIndexModel: IndexModel<ClientIndexItem, Client>
{
}
I instantiate in and return a ClientIndexModel from an ApiController as follows:
public ClientIndexModel Get()
{
var model = new ClientIndexModel();
return model;
}
If I inspect model with a breakpoint on the return model; line, the Items property is present, with a count of 0. Yet the JSON returned from this action only has the SelectListDictionary property and no Items property. Why could this be?
Your Items property has a private setter. Properties with private setters are intentionally omitted from serialization as it makes no sense to serialize them because they can never be deserialized back as their values cannot be modified from the outside. So you should either completely remove the setter (as you have done for the SelectListDictionary property), make it public or write a custom formatter using some custom serializer that is capable of serializing properties with private setters.
Related
This question has been asked before on SO and elsewhere in the context of MVC3 and there are bits and bobs about it related to ASP.NET Core RC1 and RC2 but niot a single example that actually shows how to do it the right way in MVC 6.
There are the following classes
public abstract class BankAccountTransactionModel {
public long Id { get; set; }
public DateTime Date { get; set; }
public decimal Amount { get; set; }
public readonly string ModelType;
public BankAccountTransactionModel(string modelType) {
this.ModelType = modelType;
}
}
public class BankAccountTransactionModel1 : BankAccountTransactionModel{
public bool IsPending { get; set; }
public BankAccountTransactionModel1():
base(nameof(BankAccountTransactionModel1)) {}
}
public class BankAccountTransactionModel2 : BankAccountTransactionModel{
public bool IsPending { get; set; }
public BankAccountTransactionModel2():
base(nameof(BankAccountTransactionModel2)) {}
}
In my controller I have something like this
[Route(".../api/[controller]")]
public class BankAccountTransactionsController : ApiBaseController
{
[HttpPost]
public IActionResult Post(BankAccountTransactionModel model) {
try {
if (model == null || !ModelState.IsValid) {
// failed to bind the model
return BadRequest(ModelState);
}
this.bankAccountTransactionRepository.SaveTransaction(model);
return this.CreatedAtRoute(ROUTE_NAME_GET_ITEM, new { id = model.Id }, model);
} catch (Exception e) {
this.logger.LogError(LoggingEvents.POST_ITEM, e, string.Empty, null);
return StatusCode(500);
}
}
}
My client may post either BankAccountTransactionModel1 or BankAccountTransactionModel2 and I would like to use a custom model binder to determine which concrete model to bind based on the value in the property ModelType which is defined on the abstract base class BankAccountTransactionModel.
Thus I have done the following
1) Coded up a simple Model Binder Provider that checks that the type is BankAccountTransactionModel. If this is the case then an instance of BankAccountTransactionModelBinder is returned.
public class BankAccountTransactionModelBinderProvider : IModelBinderProvider {
public IModelBinder GetBinder(ModelBinderProviderContext context) {
if (context == null) throw new ArgumentNullException(nameof(context));
if (context.Metadata.IsComplexType && !context.Metadata.IsCollectionType) {
var type1 = context.Metadata.ModelType;
var type2 = typeof(BankAccountTransactionModel);
// some other code here?
// tried this but not sure what to do with it!
foreach (var property in context.Metadata.Properties) {
propertyBinders.Add(property, context.CreateBinder(property));
}
if (type1 == type2) {
return new BankAccountTransactionModelBinder(propertyBinders);
}
}
return null;
}
}
2) Coded up the BankAccountTransactionModel
public class BankAccountTransactionModelBinder : IModelBinder {
private readonly IDictionary<ModelMetadata, IModelBinder> _propertyBinders;
public BankAccountTransactionModelBinder(IDictionary<ModelMetadata, IModelBinder> propertyBinders){
this._propertyBinders = propertyBinders;
}
public Task BindModelAsync(ModelBindingContext bindingContext) {
if (bindingContext == null) throw new ArgumentNullException(nameof(bindingContext));
// I would like to be able to read the value of the property
// ModelType like this or in some way...
// This does not work and typeValue is...
var typeValue = bindingContext.ValueProvider.GetValue("ModelType");
// then once I know whether it is a Model1 or Model2 I would like to
// instantiate one and get the values from the body of the Http
// request into the properties of the instance
var model = Activator.CreateInstance(type);
// read the body of the request in some way and set the
// properties of model
var key = some key?
var result = ModelBindingResult.Success(key, model);
// Job done
return Task.FromResult(result);
}
}
3) Lastly I register the provider in Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc(options => {
options.ModelBinderProviders.Insert(0, new BankAccountTransactionModelBinderProvider());
options.Filters.Add(typeof (SetUserContextAttribute));
});
The whole thing seems OK in that the provider is actually invoked and the same is the case for the model builder. However, I cannot seem to get anywhere with coding the logic in BindModelAsync of the model binder.
As already stated by the comments in the code, all that I'd like to do in my model binder is to read from the body of the http request and in particular the value of ModelType in my JSON. Then on the bases of that I'd like to instantiate either BankAccountTransactionModel1 or BankAccountTransactionModel and finally assign values to the property of this instance by reading them of the JSON in the body.
I know that this is a only a gross approximation of how it should be done but I would greatly appreciate some help and perhaps example of how this could or has been done.
I have come across examples where the line of code below in the ModelBinder
var typeValue = bindingContext.ValueProvider.GetValue("ModelType");
is supposed to read the value. However, it does not work in my model binder and typeValue is always something like below
typeValue
{}
Culture: {}
FirstValue: null
Length: 0
Values: {}
Results View: Expanding the Results View will enumerate the IEnumerable
I have also noticed that
bindingContext.ValueProvider
Count = 2
[0]: {Microsoft.AspNetCore.Mvc.ModelBinding.RouteValueProvider}
[1]: {Microsoft.AspNetCore.Mvc.ModelBinding.QueryStringValueProvider}
Which probably means that as it is I do not stand a chance to read anything from the body.
Do I perhaps need a "formatter" in the mix in order to get desired result?
Does a reference implementation for a similar custom model binder already exist somewhere so that I can simply use it, perhaps with some simple mods?
Thank you.
ASP.NET Core introduced custom tag helpers which can be used in views like this:
<country-select value="CountryCode" />
However, I don't understand how can I get model property name in my classes:
public class CountrySelectTagHelper : TagHelper
{
[HtmlAttributeName("value")]
public string Value { get; set; }
public override void Process(TagHelperContext context, TagHelperOutput output)
{
...
// Should return property name, which is "CountryCode" in the above example
var propertyName = ???();
base.Process(context, output);
}
}
In the previous version I was able to do this by using ModelMetadata:
var metadata = ModelMetadata.FromLambdaExpression(expression, html.ViewData);
var property = metadata.PropertyName; // return "CountryCode"
How can I do the same in the new ASP.NET tag helpers?
In order to get property name, you should use ModelExpression in your class instead:
public class CountrySelectTagHelper : TagHelper
{
public ModelExpression For { get; set; }
public override void Process(TagHelperContext context, TagHelperOutput output)
{
var propertyName = For.Metadata.PropertyName;
var value = For.Model as string;
...
base.Process(context, output);
}
}
You can pass a string via the tag helper attribute.
<country-select value="#Model.CountryCode" />
The Value property will be populated by Razor with the value of Model.CountryCode by prepending #. So you get the value directly without the need to pass the name of a model property and accessing that afterwards.
I am not sure whether you got what you wanted. If you are looking to pass the complete model from view to the custom tag helper, this is how i do it.
You can pass in your current model from the view using any custom attributes. See the example below.
Assuming your model is Country
public class Country
{
public string Name { get; set; }
public string Code { get; set; }
}
Now declare a property in your custom tag helper of the same type.
public Country CountryModel { get; set; }
Your controller would look something like this
public IActionResult Index()
{
var country= new Country
{
Name = "United States",
Code = "USA"
};
return View("Generic", country);
}
In this setup, to access your model inside the taghelper, just pass it in like any other custom attribute/property
Your view should now look like something like this
<country-select country-model="#Model"></country-select>
You can receive it in your tag helper like any other class property
public override void Process(TagHelperContext context, TagHelperOutput output)
{
...
// Should return property name, which is "CountryCode" in the above example
var propertyName = CountryModel.Name;
base.Process(context, output);
}
Happy coding!
I know how to create a model class that mirrors query string variables so that when it comes into my Web API controller action, the model is populated.
However, is there a way to make it so that I'm not locked into the query string variable names as the properties on my model class?
Example:
public class MyModel {
public string o {get;set;}
}
public class MyController {
public string Get(MyModel model) {
}
}
Then, if my query string looks like:
GET http://domain.com/?o=12345
Is there a way to name that model property "Order" or something instead of "o" and then have it populated with the value from "o="?
You can create custom model binder that will bind data to model as you wish. To use it you should:
public string Get([ModelBinder(typeof(MyComplexTypeModelBinder))]MyModel model)
{
...
}
To create custom model binder you can inherit from IModelBinder or from DefaultModelBinder.
public class MyComplexTypeModelBinder : IModelBinder
{
public Object BindModel(ControllerContext controllerContext,
ModelBindingContext bindingContext)
{
if (bindingContext == null)
throw new ArgumentNullException("bindingContext");
// Create the model instance (using the ctor you like best)
var obj = new MyComplexType();
// Set properties reading values from registered value providers
obj.Order = FromPostedData<string>(bindingContext, "o");
...
return obj;
}
private T FromPostedData<T>(ModelBindingContext context, String key)
{
// Get the value from any of the input collections
ValueProviderResult result;
context.ValueProvider.TryGetValue(key, out result);
// Set the state of the model property resulting from
context.ModelState.SetModelValue(key, result);
// Return the value converted (if possible) to the target type
return (T) result.ConvertTo(typeof(T));
}
Solution for this scenario is custom IValueProvider. This ASP.NET MVC extension point is the correct place, where we can bridge the QueryString keys into Model.Property names. In comparison with ModelBinder, this will target exactly what we need (while not introducing later issues, when even other value providers (FORM) accidently contains that key...)
There is good tutorial how to introduce the custom IValueProvider:
http://donovanbrown.com/post/How-to-create-a-custom-Value-Provider-for-MVC.aspx
And there is an simple example which is able to provide values for Model "Order" property, coming as QueryString "o" key:
Factory
// Factory
public class MyValueProviderFactory : ValueProviderFactory
{
public override IValueProvider GetValueProvider(ControllerContext ctx)
{
return new MyValueProvider(ctx);
}
}
Provider
// Provider
class MyValueProvider : IValueProvider
{
protected HttpRequestBase Request { get; set; }
public MyValueProvider(ControllerContext ctx)
{
Request = ctx.HttpContext.Request;
}
// our custom logic to test QueryString keys, and expected prefixes
public bool ContainsPrefix(string prefix)
{
var containsSpecial =
"Order".Equals(prefix, StringComparison.OrdinalIgnoreCase)
&& Request.QueryString.AllKeys.Contains("o"
, StringComparer.InvariantCultureIgnoreCase);
return containsSpecial;
}
// Handling "Order" key
public ValueProviderResult GetValue(string key)
{
if (!ContainsPrefix(key))
{
return null;
}
var values = Request.QueryString.GetValues("o");
if (values.Any())
{
return new ValueProviderResult(values, values.First()
, CultureInfo.CurrentCulture);
}
return null;
}
}
And in the global.asax we have to inject it:
protected void Application_Start()
{
ValueProviderFactories.Factories.Add(new MyValueProviderFactory());
...
I don't know why I'm just now noticing this behavior for the first time. Looking to confirm whether this is designed behavior, or whether I'm missing something.
Say I have a viewmodel that implements IEnumerable<T>, and provides additional properties. For example:
public class MyResultsViewModel : IEnumerable<MyResultViewModel>
{
public IEnumerable<MyResultViewModel> Results { get; set; }
public string SomeAdditionalProperty { get; set; }
public IEnumerator<MyResultViewModel> GetEnumerator()
{
return Results.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator; }
}
Say I also have a controller which returns this model as json. For example:
public ActionResult MyResults()
{
var entities = PrivateMethodToGetEntities();
var models = Mapper.Map<MyResultsViewModel>(entities);
models.SomeAdditionalProperty = "I want this in the JSON too";
return Json(models, JsonRequestBehavior.AllowGet);
// models now contains a populated Results as well as the add'l string prop
}
When I get the results back from a JSON request on the client, it always comes back as an array. For example:
$.get('/Path/To/MyResults')
.success(function (results) {
alert(results.SomeAdditionalProperty); // this alerts 'undefined'
alert(results.length); // this alerts the size / count of Results
alert(results[0]); // this alerts object
// inspecting results here shows that it is a pure array, with no add'l props
});
Before I refactor to make the viewmodel not implement IEnumerable<T>, I want to get some confirmation that this is by design and should be expected. I guess it makes sense, since the javascript array object's prototype would have to be extended to accommodate the additional properties.
Update:
I've made the following changes to the viewmodel, to avoid naming the internal enumerable Results:
public class MyResultsViewModel : IEnumerable<MyResultViewModel>
{
public IEnumerable<MyResultViewModel> NotNamedResults { get; set; }
public string SomeAdditionalProperty { get; set; }
public IEnumerator<MyResultViewModel> GetEnumerator()
{
return NotNamedResults.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator; }
}
With this change, the behavior remains. alert(JSON.stringify(results)) yields normal array syntax for my enumerated collection of MyResultViewModels:
[{"ResultProp1":"A","ResultProp2":"AA","ResultProp3":"AAA"},{"ResultProp1":"B","ResultProp2":"BB","ResultProp3":"BBB"},{"ResultProp1":"C","ResultProp2":"CC","ResultProp3":"CCC"},{"ResultProp1":"D","ResultProp2":"DD","ResultProp3":"DDD"},{"ResultProp1":"E","ResultProp2":"EE","ResultProp3":"EEE"}]
Is still seems that the additional property is being lost between when the controller action returns the JsonResult and the jquery success function is invoked.
For classes implementing IEnumerable, the JavascriptSerializer only serializes the enumerated items. It calls GetEnumerator() to get the data to be serialized, any other properties are ignored. That's why the Count property of a List<T> is not serialized, nor will any other property.
The reason is that this type of construction cannot be represented in json format. To include the other properties the serializer would have to create a hash object instead of an array, but a hash object is not a collection, strictly speaking. (The exception are key/value pair classes like Dictionary, which are serialized as hash objects. But the rule stands - only the enumerated dictionary entries are serialized).
But why are you creating a class that implements IEnumerable directly to serialize as json instead of doing this?
public class MyResultsViewModel {
public IEnumerable<MyModel> Models{ get; set; }
public String SomeAdditionalData { get; set; }
}
I try to Lazy< SelectList > for lazy caching any lookup data in my ASP.NET MVC project. But I cannot force Lazy object to reload lookup data when it is changed.
I create derived class like the following code. I found that Lazy< T > use IsValueCreated property to keep current state. However, in MappingFunc method I cannot change value of IsValueCreated because it is static method.
public class LazySelectList : Lazy<SelectList>
{
public LazySelectList(Func<LimeEntities, IEnumerable> initFn, string dataValueField, string dataTextField)
: base(MapingFunc(initFn, dataValueField, dataTextField))
{
}
public new bool IsValueCreated { get; set; }
public static Func<SelectList> MapingFunc(Func<DbContext, IEnumerable> valueFactory, string dataValueField, string dataTextField)
{
return () =>
{
var context = ObjectFactory.GetInstance<DbContext>();
return new SelectList(valueFactory(context), dataValueField, dataTextField);
};
}
}
I use the below code the call this function. But it always creates new value because IsValueCreated value is always false.
LookupCache.DocTypeList = new LazySelectList(db => db.DocTypes.OrderBy(x => x.Name), "ID", "Name");
After a several hours for searching & testing, I think it is impossible to reset state of lazy object. But I can create wrapper for handling this problem. The wrapper class contains lazy object and necessary object for creating new lazy object. The code should be like this.
public class LazyCache<TSource, TModel> : IValue<TModel>
{
private Lazy<TModel> _lazyObj;
private readonly Func<TSource, TModel> _valueFactory;
protected LazyCache()
{
Reset();
}
public LazyCache(Func<TSource, TModel> valueFactory) : this()
{
_valueFactory = valueFactory;
}
public void Reset()
{
_lazyObj = new Lazy<TModel>(MapingFunc());
}
public TModel Value
{
get { return _lazyObj.Value; }
}
protected virtual Func<TModel> MapingFunc()
{
return () =>
{
var context = ObjectFactory.GetInstance<TSource>();
return _valueFactory(context);
};
}
}
The above code allows us to reset state of object to force it to retrieve new data for defined function.
After that, I try to use the above method to cache SelectList object in ASP.NET MVC. But it always retrieves new from database because SelectList will contain IEnumerable object instead of real object data. So, I solve problem by enumerating data into temp object list like the following class.
public class LazyList<TSource> : LazyCache<TSource, SelectList>
{
private readonly Func<TSource, IEnumerable> _valueFactory;
private readonly string _dataValueField;
private readonly string _dataTextField;
public LazyList(Func<TSource, IEnumerable> valueFactory, string dataValueField, string dataTextField)
{
_valueFactory = valueFactory;
_dataValueField = dataValueField;
_dataTextField = dataTextField;
}
protected override Func<SelectList> MapingFunc()
{
return () =>
{
var context = ObjectFactory.GetInstance<TSource>();
// Force to retrieve data from current IEnumerable to prevent lazy loading data that
// cause system always connect to database every time they generate data from selectlist.
var loop = _valueFactory(context).GetEnumerator();
var tempList = new List<object>();
while (loop.MoveNext())
{
tempList.Add(loop.Current);
}
return new SelectList(tempList, _dataValueField, _dataTextField);
};
}
}
PS. All source code are a part of my Higgs RIA framework that available on Codeplex website.
LazyCache.cs | LazyList.cs