Get a model's property value from within a custom attribute - asp.net-mvc-3

I have a custom attribute called FileLink that uses DisplayForModel() to generate an ActionLink to a controller with an ID value.
The attribute looks like this:
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public sealed class FileLinkAttribute : Attribute
{
public string ActionName { get; set; }
public string ControllerName { get; set; }
public string IdPropertyName { get; set; }
public FileLinkAttribute(string actionName, string controllerName, string idPropertyName)
{
ActionName = actionName;
ControllerName = controllerName;
IdPropertyName = idName;
}
}
Is it used in a viewmodel, like so:
public class MyViewModel
{
[HiddenInput(DisplayValue = false)]
public int? Id { get; set; }
[HiddenInput(DisplayValue = false)]
public int? TargetId { get; set; }
[FileLink("SomeAction", "SomeController", "TargetId")]
public string Name { get; set; }
}
I have a custom MetadataProvider derived from DataAnnotationsModelMetadataProvider that puts the attribute values into the metadata AdditionalValues as so:
// retrieve the values to generate a file link
var fileLinkAttributes = attributes.OfType<FileLinkAttribute>();
if (fileLinkAttributes.Any())
{
var fileLinkAttribute = fileLinkAttributes.First();
metadata.AdditionalValues["FileLinkAttribute"] = fileLinkAttribute;
metadata.TemplateHint = "FileLink";
}
The goal is to have DisplayForModel call the DisplayTemplate below to generate a link with the value in TargetId from MyViewModel. So if TargetId is 5, the link will be "SomeController/SomeAction/5"
// This DisplayTemplate for use with the "FileLinkAttribute"
//
// Usage: [FileLinkAttribute("ActionName","ControllerName","IdPropertyName")]
var fileLinkAttribute = (FileLinkAttribute)ViewData.ModelMetadata.AdditionalValues["FileLinkAttribute"];
var targetId = *<GET VALUE OF CONTAINER MODEL IdPropertyName>*;
#Html.ActionLink((string)ViewData.Model, fileLinkAttribute.ActionName, fileLinkAttribute.ControllerName, new { Id = targetId}, null)
With all this in place, I am unable to access the containing object to pull the property value from TargetId. I have tried to do this within the Attribute, within the Provider, and within the DisplayTemplate, with no luck.
Is there a way or place I can access this value to accomplish my intent to read a property value from the viewmodel object containing the attribute?

After reading this post describing the purpose of modelAccessor in a custom DataAnnotationsModelMetadataProvider:
What is the "Func<object> modelAccessor" parameter for in MVC's DataAnnotationsModelMetadataProvider?
I was able to piece together this (very hackish) solution implemented in the custom provider:
// retrieve the values to generate a file link
var fileLinkAttributes = attributes.OfType<FileLinkAttribute>();
if (fileLinkAttributes.Any())
{
var fileLinkAttribute = fileLinkAttributes.First();
// modelAccessor contains a pointer the container, but the type is not correct but contains the name of the correct type
if (modelAccessor.Target.GetType().ToString().Contains("System.Web.Mvc.AssociatedMetadataProvider"))
{
// get the container model for this metadata
FieldInfo containerInfo = modelAccessor.Target.GetType().GetField("container");
var containerObject = containerInfo.GetValue(modelAccessor.Target);
// get the value of the requested property
PropertyInfo pi = containerObject.GetType().GetProperty(fileLinkAttribute.IdPropertyName);
string idPropertyValue = pi.GetValue(containerObject, null).ToString();
fileLinkAttribute.IdPropertyValue = idPropertyValue;
}
metadata.AdditionalValues["FileLinkAttribute"] = fileLinkAttribute;
metadata.TemplateHint = "FileLink";
}
I am not sure how to derefence a Func to get it's actual target, and the type name comes out all munged wierdly in this example, such as
"System.Web.Mvc.AssociatedMetadataProvider+<>c__DisplayClassb"
So I convert to a string to check if it contains "System.Web.Mvc.AssociatedMetadataProvider". Probably not the best thing but this is the only way I could get it to work.
Oh, I also added a 4th property to the attribute to contain the value:
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public sealed class FileLinkAttribute : Attribute
{
public string ActionName { get; set; }
public string ControllerName { get; set; }
public string IdPropertyName { get; set; }
public string IdPropertyValue { get; set; }
public FileLinkAttribute(string actionName, string controllerName, string idPropertyName)
{
ActionName = actionName;
ControllerName = controllerName;
IdPropertyName = idPropertyName;
}
}
and in the DisplayTemplate I do:
var modelId = fileLinkAttribute.IdPropertyValue;

This should be an editor template for strsing or object as that'swhat you are applying the attribute on. So inside ~/Views/Shared/EditorTemplates/string.cshtml:
#model string
#{
var fileLinkAttribute = (FileLinkAttribute)ViewData.ModelMetadata.AdditionalValues["FileLinkAttribute"];
var targetId = fileLinkAttribute.IdPropertyName;
}
#Html.ActionLink(Model, fileLinkAttribute.ActionName, fileLinkAttribute.ControllerName, new { Id = targetId}, null)
and in your main view:
#Html.EditorFor(x => x.Name)

Related

How to display complex class data in ASP.NET Core

This is my complex class
namespace EnergaticWebApp.Models
{
public class MyModel
{
public string MyString { get; set; }
public int MyInt { get; set; }
public bool MyBoolean { get; set; }
public decimal MyDecimal { get; set; }
public DateTime MyDateTime1 { get; set; }
public DateTime MyDateTime2 { get; set; }
public List<string> MyStringList { get; set; }
public Dictionary<string, Person> MyDictionary { get; set; }
public MyModel MyAnotherModel { get; set; }
}
public class Person
{
public int Id { get; set; }
public string Name { get; set; }
}
}
And this is my action method in which I want to display my complex class data
public IActionResult Privacy()
{
var options = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = true
};
var folderDetails = Path.Combine(Directory.GetCurrentDirectory(), $"wwwroot\
{"json\\my-model.json"}");
var jsonString = System.IO.File.ReadAllText(folderDetails);
// var jsonString = File.ReadAllText("my-model.json");
var jsonModel = JsonSerializer.Deserialize<MyModel>(jsonString, options);
var modelJson = JsonSerializer.Serialize(jsonModel, options); //not in use
return View(jsonModel);
}
}
Everything is working fine except when I am returning my jsonModel in view I get an error:
InvalidOperationException: The model item passed into the ViewDataDictionary is of type 'EnergaticWebApp.Models.MyModel', but this ViewDataDictionary instance requires a model item of type 'System.Collections.Generic.List`1[EnergaticWebApp.Models.MyModel]'
Any suggestions of how to do this?
The error is pretty clear - you pass MyModel into the view (jsonModel); but the view expects List<MyModel> instead. They have to match!
Try to change your model in the Privacy view
#model EnergaticWebApp.Models.MyModel
InvalidOperationException: The model item passed into the
ViewDataDictionary is of type 'EnergaticWebApp.Models.MyModel', but
this ViewDataDictionary instance requires a model item of type
'System.Collections.Generic.List`1[EnergaticWebApp.Models.MyModel]'
This error message means your view needs type of List<MyModel> but you pass type of MyModel in backend.
The first way is to change your view required model, but this way may cause you need change a lot for your html code:
#model EnergaticWebApp.Models.MyModel
The second way is to change your return model to List<MyModel>.You can create an instance for List<MyModel> and add your jsonModel to it:
var jsonModel = JsonSerializer.Deserialize<MyModel>(jsonString, options);
var data = new List<MyModel>(); //add this..
data.Add(jsonModel);
return View(data);
when am converting into list: var jsonModel = JsonSerializer.Deserialize<List>(jsonString, options); its give me this error now: The JSON value could not be converted to System.Collections.Generic.List`1[EnergaticWebApp.Models.MyModel]. Path: $ | LineNumber: 0 | BytePositionInLine: 1
Only your json like below can be deserialized to List<MyModel>. If you allow changing the json, you can use the third way(var jsonModel = JsonSerializer.Deserialize<List<MyModel>>(jsonString, options);)
[
{
"MyString": "sad",
"MyInt": 1
//more properties...
}
]

Web Api 2 Serialize data annotations

I have a domain object Owner. This has some data annotations which will be used in the web api post method to validate.
public class Owner
{
[Required(ErrorMessage ="Please select a title")]
public string Title { get; set; }
[Required(ErrorMessage = "First name is required")]
[MaxLength(100, ErrorMessage ="Too long")]
public string Firstname { get; set; }
[Required(ErrorMessage = "Last name is required")]
public string Lastname { get; set; }
public string PostalAddressStreet { get; set; }
public string PostalAddressSuburb { get; set; }
public string PostalAddressState { get; set; }
}
Now i need to send this object's validation rules (defined in data annotations) to the front end in a get request. I was looking at this question which explains how to do this in MVC. But couldnt get it to working in a web api Get method. This is what i tried.
[HttpGet]
[Route("GetOwnerDefinition")]
public string GetPetOwnerDefinition()
{
Owner owner = new Owner();
System.Web.Http.Metadata.Providers.DataAnnotationsModelMetadataProvider metaProvider = new System.Web.Http.Metadata.Providers.DataAnnotationsModelMetadataProvider();
var metaData = metaProvider.GetMetadataForProperty(null, typeof(Owner), "Firstname");
var validationRules = metaData.GetValidators(GlobalConfiguration.Configuration.Services.GetModelValidatorProviders());
foreach(System.Web.Http.Validation.Validators.DataAnnotationsModelValidator modelValidator in validationRules)
{
//need help here
}
At the end of the day i need to generate a JSON definition as below.
{"Firstname": "John",
"ValidationRules":[{"data-val-required":"This field is required.", "data-val-length-max":100}]}
Not sure whether it is the best way, this is what i have done. I have created a new instance of MVC context inside a web api method.
var mvcContext = new System.Web.Mvc.ControllerContext();
See the code below.
public Dictionary<string, string> GetValidationDefinition(object container, Type type)
{
var modelMetaData = System.Web.Mvc.ModelMetadataProviders.Current.GetMetadataForProperties(container, type);
var mvcContext = new System.Web.Mvc.ControllerContext();
var validationAttributes = new Dictionary<string, string>();
foreach (var metaDataForProperty in modelMetaData)
{
var validationRulesForProperty = metaDataForProperty.GetValidators(mvcContext).SelectMany(v => v.GetClientValidationRules());
foreach (System.Web.Mvc.ModelClientValidationRule rule in validationRulesForProperty)
{
string key = metaDataForProperty.PropertyName + "-" + rule.ValidationType;
validationAttributes.Add(key, System.Web.HttpUtility.HtmlEncode(rule.ErrorMessage ?? string.Empty));
key = key + "-";
foreach (KeyValuePair<string, object> pair in rule.ValidationParameters)
{
validationAttributes.Add(key + pair.Key, System.Web.HttpUtility.HtmlAttributeEncode(pair.Value != null ? Convert.ToString(pair.Value, System.Globalization.CultureInfo.InvariantCulture) : string.Empty));
}
}
}
return validationAttributes;
}

Sending model data from Knockout back to MVC 3

I am new to knockout and am having a hard time trying to get my data from Knockout back to my server. I keep getting an error of 'No parameterless constructor defined for this object.' Any help would be appreciated.
My Knockout model is as follows
function partSummary(item) {
var self = this;
self.ID = ko.observable(item.ID);
self.serialNumber = ko.observable(item.SerialNumber);
self.description = ko.observable(item.Description);
self.manufacturer = ko.observable(item.Manufacturer);
self.creationDate = ko.observable(item.DateCreated);
self.active = ko.observable(item.IsActive);
}
My code to send the data back the server is
self.savePart = function() {
$.ajax("/PartKO/UpdatePart", {
data: ko.toJSON(self.partDetails),
type: "post",
contentType: 'application/json',
dataType: 'json'
});
};
My MVC controller is
[HttpPost]
public JsonResult UpdatePart(PartDetail part)
{
var dbPart = new PartGenericAccessor();
dbPart.ID = part.ID;
dbPart.Load();
dbPart.Description = part.Description;
dbPart.IsActive = Convert.ToBoolean(part.IsActive);
var manufacturers = ManufacturerAccessor.LoadAll<ManufacturerAccessor>();
if (part.Manufacturer != null)
{
var manufacturer = (from p in manufacturers where p.Name == part.Manufacturer select p.ID).First();
dbPart.ManufacturerID = manufacturer;
}
dbPart.Save();
return Json("Success!!");
}
And my server side model is
public class PartDetail
{
public PartDetail(Guid id, string serial, string description, string manufacturer, DateTime created, bool isActive)
{
ID = id;
SerialNumber = serial;
Description = description;
Manufacturer = manufacturer;
DateCreated = created.ToShortDateString();
IsActive = isActive.ToString(CultureInfo.InvariantCulture);
}
public Guid ID { get; set; }
public string SerialNumber { get; set; }
public string Description { get; set; }
public string Manufacturer { get; set; }
public string DateCreated { get; set; }
public string IsActive { get; set; }
}
You need to supply a parameterless constructor for your MVC model:
public class PartDetail
{
public PartDetail()
{ ... }
}
When the data comes back from the server, MVC will create an empty object, using the parameterless constructor and then call the 'set' methods to set each property that matches the data coming in.
Once I made stupid mistake:
Named controller argument 'action' - and on post it allways null.
[HttpPost]
public JsonResult AddMetaAction(ActionModel action)
I didn't know about that and spend on debug and solving that problem about a half of hour:(

How to exclude a field from #Html.EditForModel() but have it show using Html.DisplayForModel()

I am reading up on ASP.NET MVC and all of it's fun uses and I just found out about DataTemplates.
In my hurry to test this thing out, I converted one of my simpler models over to using #Html.DisplayForModel() and #Html.EditForModel() and it worked like a lucky charm that it is :)
One thing that I immediately found out though was that I could not easily define a field to show up on display views but not be present at all for editing...
You can make use of IMetadataAware interface an create attribute which will set ShowForEdit and ShowForDislay in Metadata:
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public class TemplatesVisibilityAttribute : Attribute, IMetadataAware
{
public bool ShowForDisplay { get; set; }
public bool ShowForEdit { get; set; }
public TemplatesVisibilityAttribut()
{
this.ShowForDisplay = true;
this.ShowForEdit = true;
}
public void OnMetadataCreated(ModelMetadata metadata)
{
if (metadata == null)
{
throw new ArgumentNullException("metadata");
}
metadata.ShowForDisplay = this.ShowForDisplay;
metadata.ShowForEdit = this.ShowForEdit;
}
}
Then you can attach it to your property like this:
public class TemplateViewModel
{
[TemplatesVisibility(ShowForEdit = false)]
public string ShowForDisplayProperty { get; set; }
public string ShowAlwaysProperty { get; set; }
}
And this is all you need.
You could write a custom metadata provider and set the ShowForEdit metadata property. So start with a custom attribute:
public class ShowForEditAttribute : Attribute
{
public ShowForEditAttribute(bool show)
{
Show = show;
}
public bool Show { get; private set; }
}
then a custom model metadata provider:
public class MyModelMetadataProvider : DataAnnotationsModelMetadataProvider
{
protected override ModelMetadata CreateMetadata(
IEnumerable<Attribute> attributes,
Type containerType,
Func<object> modelAccessor,
Type modelType,
string propertyName
)
{
var metadata = base.CreateMetadata(attributes, containerType, modelAccessor, modelType, propertyName);
var sfea = attributes.OfType<ShowForEditAttribute>().FirstOrDefault();
if (sfea != null)
{
metadata.ShowForEdit = sfea.Show;
}
return metadata;
}
}
then register this provider in Application_Start:
ModelMetadataProviders.Current = new MyModelMetadataProvider();
and finally decorate:
public class MyViewModel
{
[ShowForEdit(false)]
public string Prop1 { get; set; }
public string Prop2 { get; set; }
}
Now if in your view you have:
#model MyViewModel
<h2>Editor</h2>
#Html.EditorForModel()
<h2>Display</h2>
#Html.DisplayForModel()
the Prop1 property won't be included in the editor template.
Remark: you could do the same with the ShowForDisplay metadata property.
Can you display each of the fields you want using Html.DisplayTextbox or one of the other options? That way you can also customize the appearance and labels referring to the field.

Validation Attributes MVC 2 - checking one of two values

Could someone help me with this issue. I'm trying to figure out how to check two values on a form, one of the two items has to be filled in. How do I do a check to ensure one or both of the items have been entered?
I'm using viewmodels in ASP.NET MVC 2.
Here's a little snip of code:
The view:
Email: <%=Html.TextBoxFor(x => x.Email)%>
Telephone: <%=Html.TextBoxFor(x => x.TelephoneNumber)%>
The viewmodel:
[Email(ErrorMessage = "Please Enter a Valid Email Address")]
public string Email { get; set; }
[DisplayName("Telephone Number")]
public string TelephoneNumber { get; set; }
I want either of these details to be provided.
Thanks for any pointers.
You can probably do this in much the same way as the PropertiesMustMatch attribute that comes as part of the File->New->ASP.NET MVC 2 Web Application.
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)]
public sealed class EitherOrAttribute : ValidationAttribute
{
private const string _defaultErrorMessage = "Either '{0}' or '{1}' must have a value.";
private readonly object _typeId = new object();
public EitherOrAttribute(string primaryProperty, string secondaryProperty)
: base(_defaultErrorMessage)
{
PrimaryProperty = primaryProperty;
SecondaryProperty = secondaryProperty;
}
public string PrimaryProperty { get; private set; }
public string SecondaryProperty { get; private set; }
public override object TypeId
{
get
{
return _typeId;
}
}
public override string FormatErrorMessage(string name)
{
return String.Format(CultureInfo.CurrentUICulture, ErrorMessageString,
PrimaryProperty, SecondaryProperty);
}
public override bool IsValid(object value)
{
PropertyDescriptorCollection properties = TypeDescriptor.GetProperties(value);
object primaryValue = properties.Find(PrimaryProperty, true /* ignoreCase */).GetValue(value);
object secondaryValue = properties.Find(SecondaryProperty, true /* ignoreCase */).GetValue(value);
return primaryValue != null || secondaryValue != null;
}
}
The key part of this function is the IsValid function that determines if one of the two parameters has a value.
Unlike normal Property-based attributes, this is applied to the class level and can be used like so:
[EitherOr("Email", "TelephoneNumber")]
public class ExampleViewModel
{
[Email(ErrorMessage = "Please Enter a Valid Email Address")]
public string Email { get; set; }
[DisplayName("Telephone Number")]
public string TelephoneNumber { get; set; }
}
You should be able to add as many as these as you need per form, but if you want to force them to enter a value into one of more than two boxes (Email, Telephone or Fax for example), then you would probably be best changing the input to be more an array of values and parse it that way.

Resources