POST data to action using HtmlFieldPrefix yields empty model - c#

In my application, I have a few modal windows. Each of them takes the same partial view and the same view model, but displays other data. For that, I generate a dynamic HtmlFieldPrefix as to not have multiple IDs of the same name. Example:
#foreach (var product in Model.Products)
{
string buyModalId = product.BuyModel.BindingPrefix;
#await Html.PartialForAsync("_BuyForm", product.BuyModel, buyModalId)
}
BindingPrefix contains a dynamically added string (for example buy-product-{ID}). In my view I also have a hidden field that is supposed to POST the binding prefix back:
#Html.Hidden(nameof(Model.BindingPrefix), Model.BindingPrefix)
(Source: Asp.Net MVC Dynamic Model Binding Prefix)
That does not work, however, since the binding prefix is null when POSTing, too. Hence await TryUpdateModelAsync(model, model.BindingPrefix); in my controller fails.
The code for the Html.PartialForAsync method is the following:
public static Task<IHtmlContent> PartialForAsync(this IHtmlHelper htmlHelper, string partialViewName, object model, string prefix)
{
var viewData = new ViewDataDictionary(htmlHelper.ViewData);
var htmlPrefix = viewData.TemplateInfo.HtmlFieldPrefix;
viewData.TemplateInfo.HtmlFieldPrefix += !Equals(htmlPrefix, string.Empty) ? $".{prefix}" : prefix;
var part = htmlHelper.PartialAsync(partialViewName, model, viewData);
return part;
}
(Source: MVC 6 VNext how to set HtmlFieldPrefix?)
What am I missing? Why is my model still null? When removing the binding prefix, the binding works flawlessly - but the browser throws warnings regarding multiple same IDs.

Found the answer by using a custom model binder inside my model:
public override void BindModel(ModelBindingContext bindingContext)
{
var providers = bindingContext.ValueProvider as System.Collections.IList;
var formProvider = providers?.OfType<JQueryFormValueProvider>().FirstOrDefault();
if (formProvider != null)
{
var (_, value) = formProvider.GetKeysFromPrefix(string.Empty).First();
bindingContext.BinderModelName = value;
bindingContext.FieldName = value;
bindingContext.ModelName = value;
}
base.BindModel(bindingContext);
}

Related

Can we Access Complex object stored in session in Razor View .Net core 3.1

Is there a possibility of accessing complex object inside .cshtml in .net core 3.1 ? I went through most of the sites couldn't get any information on the same. On the server side i found we can do as below
public static class SessionExtensions
{
public static T GetComplexData<T>(this ISession session, string key)
{
var data = session.GetString(key);
if (data == null)
{
return default(T);
}
return JsonConvert.DeserializeObject<T>(data);
}
public static void SetComplexData(this ISession session, string key, object value)
{
session.SetString(key, JsonConvert.SerializeObject(value));
}
}
HttpContext.Session.SetComplexData("abcd", listOfObject);
How do we access this listOfObject which is List in session in razor view?
Regards,
Kiran
You can access the methods above in the server side code right before returning the view. That way you can create a ViewModel to store your complex object, and return this ViewModel. Now you can access your complex object within the razor view.
EX:
public IActionResult Index()
{
var myComplexObject = HttpContext.Session.GetComplexData<ComplexObjectType>("abcd");
var vM = new SomeViewModel
{
ComplexObject = myComplexObject
};
return View(vM);
}
Then in the view,
#model ProjectName.ViewModels.SomeViewModel
#{
Layout = "~/Views/Shared/_Layout.cshtml";
}
// Access it like so:
Model.ComplexObject

How can I get the metadata of the items in a collection in razor view-engine?

I have a project written in C# on the top on ASP.NET MVC 5 framework. I am trying to decouple my views from my view model so I can make my views reusable. With the heavy use of EditorTemplates I am able to create all of my standard views (i.e create, edit and details) by evaluating the ModelMetadata and the data-annotation-attributes for each property on the model, then render the page. The only thing that I am puzzled with is how to render the Index view.
My index view typically accepts an IEnumerable<object> or IPagedList<object> collection. In my view, I want to be able to evaluate the ModelMetadata of a each object/record in the collection to determine if a property on the object should be displayed or not.
In another words my view-model will look something like this
public class DisplayPersonViewModel
{
public int Id{ get; set; }
[ShowOnIndexView]
public string FirstName { get; set; }
[ShowOnIndexView]
public string LastName { get; set; }
[ShowOnIndexView]
public int? Age { get; set; }
public string Gender { get; set; }
}
Then my Index.cshtml view will accepts IPagedList<DisplayPersonViewModel> for each record in the collection, I want to display the value of the property that is decorated with ShowOnIndexView attribute.
Typically I would be able to do that my evaluating the ModelMetadata in my view with something like this
#model IPagedList<object>
#{
var elements = ViewData.ModelMetadata.Properties.Where(metadata => !metadata.IsComplexType && !ViewData.TemplateInfo.Visited(metadata))
.OrderBy(x => x.Order)
.ToList();
}
<tr>
#foreach(var element in elements)
{
var onIndex = element.ContainerType.GetProperty(element.PropertyName)
.GetCustomAttributes(typeof(ShowOnIndexView), true)
.Select(x => x as ShowOnIndexView)
.FirstOrDefault(x => x != null);
if(onIndex == null)
{
continue;
}
#Html.Editor(element.PropertyName, "ReadOnly")
}
</tr>
Then my controller will look something like this
public class PersonController : Controller
{
public ActionResult Index()
{
// This would be a call to a service to give me a collection with items. but I am but showing the I would pass a collection to my view
var viewModel = new List<DisplayPersonViewModel>();
return View(viewModel);
}
}
However the problem with evaluating ModelMetadata for the IPagedList<DisplayPersonViewModel> is that it gives me information about the collection itself not about the generic type or the single model in the collection. In another words, I get info like, total-pages, items-per-page, total-records....
Question
How can I access the ModelMetadata info for each row in the collection to be able to know which property to display and which not to?
I will preface this answer by recommending you do not pollute your view with this type of code. A far better solution would be to create a custom HtmlHelper extension method to generate the html, which gives you far more flexibility, can be unit tested, and is reusable.
The first thing you will need to change is the model declaration in the view which needs to be
#model object
otherwise you will throw this exception (List<DisplayPersonViewModel> is not IEnumerable<object> or IPagedList<object>, but it is object)
Note that it is not clear if you want the ModelMetadata for the type in the collection or for each item in the collection, so I have included both, plus code that gets the type
#model object
#{
var elements = ViewData.ModelMetadata.Properties.Where(metadata => !metadata.IsComplexType && !ViewData.TemplateInfo.Visited(metadata)).OrderBy(x => x.Order).ToList();
// Get the metadata for the model
var collectionMetaData = ViewData.ModelMetadata;
// Get the collection type
Type type = collectionMetaData.Model.GetType();
// Validate its a collection and get the type in the collection
if (type.IsGenericType)
{
type = type.GetInterfaces().Where(t => t.IsGenericType)
.Where(t => t.GetGenericTypeDefinition() == typeof(IEnumerable<>))
.Single().GetGenericArguments()[0];
}
else if (type.IsArray)
{
type = type.GetElementType();
}
else
{
// its not a valid model
}
// Get the metadata for the type in the collection
ModelMetadata typeMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, type);
}
....
#foreach (var element in collectionMetaData.Model as IEnumerable))
{
// Get the metadata for the element
ModelMetadata elementMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => element, type);
....
Note that using reflection to determine if the attribute exists in each iteration of your loop is inefficient, and I suggest you do that once (based on the typeMetadata). You could then (for example) generate an array of bool values, and then use an indexer in the loop to check the value). A better alternative would be to have your ShowOnIndexView attribute implement IMetadataAware and add a value to the AdditionalValues of ModelMetadata so that var onIndex code is not required at all. For an example of implementing IMetadataAware, refer CustomAttribute reflects html attribute MVC5

DataType mvc empty

I have a Model with properties like this:
[DataType(DataType.MultilineText)]
[MaxLength(512)]
public string Description
{
get
{
this.OnReadingDescription(ref _description);
return _description;
}
set
{
this.OnDescriptionChanging(ref value);
this._description = value;
this.OnDescriptionChanged();
}
}
and a View like this:
#using AspMvcBase.Bootstrap
#model NpoDb.Presentation.Web.Models.Media
#Html.EditForm();
EditForm is a custom HtmlHelper extension which renders every property in the ViewModel.
This part works realy fine but I want do render some of the properties different so I added the [DataType(DataType.MultilineText)] Attribute.
I want to test now in my HtmlHelper if there is an Attribute for a MultilineText.
modelMetadata.DataTypeName.Equals(DataType.EmailAddress.ToString())
But the problem is that DataTypeName is null. Even if I look at it in the View it's null.
This is one of the better resources I found about this.Brad Wilson MVC2 Template
But I can't figure out why it doesn't work in my case.
So the basic question is: How can I access Attributes in a HtmlHelper?
I would really appreciate some help. :)
If you are trying to get the attribute for a property of your model, you can do this:
public static string EditForm(this HtmlHelper helper)
{
var model = helper.ViewData.Model;
var dataType = model
.GetType()
.GetProperty("Description")
.GetCustomAttribute<DataTypeAttribute>()
.DataType;
...
}

Convert Model to ViewModel without automapper

Hi this is my convert a Ilist of model to a Ilist of ViewModel method
public static IList<PostViewModel> ConvertToPostViewModelList(this IList<Post> posts)
{
return posts.Select(ConvertToPostViewModel).ToList();
}
and also this is the ConvertToPostViewModel
public static PostViewModel ConvertToPostViewModel(this Post post)
{
var blogPostViewModel = new PostViewModel
{
Id = post.Id,
Body = post.Body,
Summary = post.Summary,
Title = post.Title,
Category = post.Category,
CreationDate = post.CreationDate,
SelectedCategory = post.CategoryId,
SelectedTag = post.TagId,
Tag = post.Tag,
UrlSlug = post.UrlSlug
};
return blogPostViewModel;
}
what is the problem with this , I got this error View :
The model item passed into the dictionary is of type 'System.Collections.Generic.List`1[Blog.Domain.Model.Post]', but this dictionary requires a model item of type 'System.Collections.Generic.IList`1[Blog.Web.UI.ViewModels.PostViewModel]'.
then ?? I convert the Ilist of Model to ViewModel via this :
return posts.Select(ConvertToPostViewModel).ToList();
then what is going on ??
what I have done in action
public ActionResult Posts()
{
var blogPost = _blogRepository.GetAllPost();
var blogPostViewModel = blogPost.ConvertToPostViewModelList();
return View("Posts", blogPostViewModel);
}
and in View
#model IList<Blog.Web.UI.ViewModels.PostViewModel>
Two possibilities:
The method you posted is not the one being matched. You can verify this easily by throwing an exception in your controller action and seeing if it gets thrown. This could be the result of either: (a) an overloaded Posts controller action, where the other is being matched; or, (b) a custom route that is intercepting the request.
You originally returned the domain object in testing, but after changing the controller action to set the model to the PostViewModel, you forgot to recompile your MVC project. Try recompiling your solution and see if the results change.

Passing arguments to Web API model binder

I am trying to create Web API model binder that will bind URL parameters sent by a grid component of a javascript framework.
Grid sends URL parameters indicating standard page, pageSize, and JSON formatted sorters, filters, and groupers.
The URL string looks like this:
http://localhost/api/inventory?page=1&start=0&limit=10sort=[{"property":"partName","direction":"desc"},{"property":"partStatus","direction":"asc"}]&group=[{"property":"count","direction":"asc"}]
The model in question is Inventory which has simple, Count (int) property and a reference, Part (Part) peoperty (which in turn has Name, Status).
The view model/dto is flattened (InventoryViewModel .Count, .PartName, .PartStatus, etc, etc.)
I use Dynamic Expression Api then to query domain model, map the result to view model and send it back as JSON.
During model binding I need to build the expressions by examining model and view model that are being used.
In order to keep model binder reusable, how can I pass/specify model and view model types being used?
I need this in order to build valid sort,filter,and grouping expsessions
Note: I don't want to pass these as part of the grid url params!
One idea I had was to make StoreRequest generic (e.g. StoreRequest) but I am not sure if or how model binder would work.
Sample API Controller
// 1. model binder is used to transform URL params into StoreRequest. Is there a way to "pass" types of model & view model to it?
public HttpResponseMessage Get(StoreRequest storeRequest)
{
int total;
// 2. domain entites are then queried using StoreRequest properties and Dynamic Expression API (e.g. "Order By Part.Name DESC, Part.Status ASC")
var inventoryItems = _inventoryService.GetAll(storeRequest.Page, out total, storeRequest.PageSize, storeRequest.SortExpression);
// 3. model is then mapped to view model/dto
var inventoryDto = _mapper.MapToDto(inventoryItems);
// 4. response is created and view model is wrapped into grid friendly JSON
var response = Request.CreateResponse(HttpStatusCode.OK, inventoryDto.ToGridResult(total));
response.Content.Headers.Expires = DateTimeOffset.UtcNow.AddMinutes(5);
return response;
}
StoreRequestModelBinder
public class StoreRequestModelBinder : IModelBinder
{
private static readonly ILog Logger = LogManager.GetCurrentClassLogger();
public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
{
Logger.Debug(m => m("Testing model binder for type: {0}", bindingContext.ModelType));
if (bindingContext.ModelType != typeof(StoreRequest))
{
return false;
}
var storeRequest = new StoreRequest();
// ----------------------------------------------------------------
int page;
if (TryGetValue(bindingContext, StoreRequest.PageParameter, out page))
{
storeRequest.Page = page;
}
// ----------------------------------------------------------------
int pageSize;
if (TryGetValue(bindingContext, StoreRequest.PageSizeParameter, out pageSize))
{
storeRequest.PageSize = pageSize;
}
// ----------------------------------------------------------------
string sort;
if (TryGetValue(bindingContext, StoreRequest.SortParameter, out sort))
{
try
{
storeRequest.Sorters = JsonConvert.DeserializeObject<List<Sorter>>(sort);
// TODO: build sort expression using model and viewModel types
}
catch(Exception e)
{
Logger.Warn(m=>m("Unable to parse sort parameter: \"{0}\"", sort), e);
}
}
// ----------------------------------------------------------------
bindingContext.Model = storeRequest;
return true;
}
private bool TryGetValue<T>(ModelBindingContext bindingContext, string key, out T result)
{
var valueProviderResult = bindingContext.ValueProvider.GetValue(key);
if (valueProviderResult == null)
{
result = default(T);
return false;
}
result = (T)valueProviderResult.ConvertTo(typeof(T));
return true;
}
}
just change your controller signature like
public HttpResponseMessage Get([ModelBinder(typeof(StoreRequestModelBinder)]StoreRequest storeRequest)
Regards

Categories

Resources