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!
Related
Can you create custom data annotations for the model that can be read inside the T4 template for the View like property.Scaffold is read? I would like to add data annotation parameters like Scaffold based on which I would build the view.
Thank you
I wrote a blog post on the solution I came up with for MVC5. I'm posting it here for anyone who comes along:
https://johniekarr.wordpress.com/2015/05/16/mvc-5-t4-templates-and-view-model-property-attributes/
Edit: In your entities, decorate property with custom Attribute
namespace CustomViewTemplate.Models
{
[Table("Person")]
public class Person
{
[Key]
public int PersonId { get; set;}
[MaxLength(5)]
public string Salutation { get; set; }
[MaxLength(50)]
public string FirstName { get; set; }
[MaxLength(50)]
public string LastName { get; set; }
[MaxLength(50)]
public string Title { get; set; }
[DataType(DataType.EmailAddress)]
[MaxLength(254)]
public string EmailAddress { get; set; }
[DataType(DataType.MultilineText)]
public string Biography { get; set; }
}
}
With this Custom Attribute
namespace CustomViewTemplate
{
[AttributeUsage(AttributeTargets.Property)]
public class RichTextAttribute : Attribute
{
public RichTextAttribute() { }
}
}
Then create a T4Helper that we'll reference in our template
using System;
namespace CustomViewTemplate
{
public static class T4Helpers
{
public static bool IsRichText(string viewDataTypeName, string propertyName)
{
bool isRichText = false;
Attribute richText = null;
Type typeModel = Type.GetType(viewDataTypeName);
if (typeModel != null)
{
richText = (RichTextAttribute)Attribute.GetCustomAttribute(typeModel.GetProperty(propertyName), typeof(RichTextAttribute));
return richText != null;
}
return isRichText;
}
}
}
So, this is how you do it.
Follow this tutorial on how to create a custom attribute http://origin1tech.wordpress.com/2011/07/20/mvc-data-annotations-and-custom-attributes/
To read this attribute values in the T4 scaffolding templates, first add the template files as described here http://www.hanselman.com/blog/ModifyingTheDefaultCodeGenerationscaffoldingTemplatesInASPNETMVC.aspx
Then, for example, open List.tt from the AddView folder. This template creates the Index view.
Go to the end of the template file and find the definition for class ModelProperty. Add your property value to it ( public string MyAttributeValue { get; set; }
Now go a bit down in the List.tt and find bool Scaffold(PropertyInfo property) method. You will need to add your own attribute property reader. This method, for the above mentioned tutorial, would be:
string OptionalAttributesValueReader(PropertyInfo property){
foreach (object attribute in property.GetCustomAttributes(true)) {
var attr = attribute as OptionalAttributes ;
if (attr != null) {
return attr.style;
}
}
return String.Empty;
}
Then find the method List GetEligibleProperties(Type type) at the bottom of the file. Add your reader to it like this:
...
IsForeignKey = IsForeignKey(prop),
IsReadOnly = prop.GetSetMethod() == null,
Scaffold = Scaffold(prop),
MyAttributeValue = OptionalAttributesValueReader(prop)
When you want to use and read this attribute you can do it like the Scaffold property is used in the List.tt
List<ModelProperty> properties = GetModelProperties(mvcHost.ViewDataType);
foreach (ModelProperty property in properties) {
if (property.MyAttributeValue != String.Empty) {
//read the value
<#= property.MyAttributeValue #>
}
}
Since these classes are defined in my project, I had to add my project dll and namespace to the top of the List.tt:
<## assembly name="C:\myProjectPath\bin\myMVCproject.dll" #>
<## import namespace="myMVCproject.CustomAttributes" #>
If your model changes and you need to find these new changes in the scaffolding, you need to rebuild your project.
Hope anyone looking for the solution will find this useful. Ask if there is anything unclear.
This is how I did it in MVC 5. I did this a long time ago and I may be forgetting stuff, I'm just copy/pasting what I see in my modified templates.
I needed a way to set the order of properties in (for example) the create/edit views or in the list view table. So I created a custom attribute OrderAttribute with an integer property Order.
To access this attribute in the T4 templates I modified the file ModelMetadataFunctions.cs.include.t4. At the top I added one method that retrieves the Order value set in the attribute from a PropertyMetadata object, and another method to simply order a list of PropertyMetadata items by that order:
List<PropertyMetadata> GetOrderedProperties(List<PropertyMetadata> properties, Type modelType) {
return properties.OrderBy<PropertyMetadata, int>(p => GetPropertyOrder(modelType, p)).ToList();
}
int GetPropertyOrder(Type type, PropertyMetadata property) {
var info = type.GetProperty(property.PropertyName);
if (info != null)
{
var attr = info.GetCustomAttribute<OrderAttribute>();
if (attr != null)
{
return attr.Order;
}
}
return int.MaxValue;
}
Finally, in the List template for example, I have added a part where I call the GetOrderedProperties method:
var typeName = Assembly.CreateQualifiedName("AcCtc, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null", ViewDataTypeName);
var modelType = Type.GetType(typeName);
var properties = ModelMetadata.Properties.Where(p => p.Scaffold && !p.IsPrimaryKey && !p.IsForeignKey && !(p.IsAssociation && GetRelatedModelMetadata(p) == null)).ToList();
properties = GetOrderedProperties(properties, modelType);
foreach (var property in properties)
{
//...
}
Unfortunately I needed the name of the project to be able to create a Type object which I needed to get the attributes from. Not ideal, perhaps you can get it some other way but I couldn't manage it without this string including all the version stuff.
I'm using a third party reporting engine (stimulsoft) that calls an action on a controller via POST. Inside of the form, many fields are sent for the mechanics of the third party. Inside of the action I need some parameters all my parameters are inside of the URL.
I want to be able to use the model binder inside of my action.
At the moment I'm getting each fields one by one using this methods
var queryString = HttpUtility.ParseQueryString(Request.UrlReferrer.Query);
var preparedBy = queryString["preparedBy"];
var preparedAt = (queryString["preparedAt"] != null) ? Convert.ToDateTime(queryString["preparedAt"]) : DateTime.Today;
I would prefer to use a model and binding using the UrlReferrer. I've created a UrlReferrerValueProvider to bind from the action. I've tried that, but I'm getting a NullReferenceException on binder.BindModel line
public class UrlReferrerValueProvider : NameValueCollectionValueProvider
{
public UrlReferrerValueProvider(ControllerContext controllerContext)
: base(HttpUtility.ParseQueryString(controllerContext.HttpContext.Request.UrlReferrer.Query), CultureInfo.InvariantCulture)
{
}
}
public ActionResultat GetReportSnapshot()
{
var bindingContext = new ModelBindingContext()
{
ValueProvider = new UrlReferrerValueProvider(ControllerContext),
ModelName = "MyReportModel",
FallbackToEmptyPrefix = true
};
var binder = new DefaultModelBinder();
var myReportModel = binder.BindModel(ControllerContext, bindingContext);
[...]
return new EmptyResult();
}
public class MyReportModel
{
public string PreparedBy {get;set;}
public DateTime PreparedAt {get;set;}
}
Edited based on comments.
public class MyReportModel
{
public string PreparedBy {get;set;}
public DateTime PreparedAt {get;set;}
}
public class UrlReferrerValueProvider : NameValueCollectionValueProvider
{
public UrlReferrerValueProvider(ControllerContext controllerContext)
: base(HttpUtility.ParseQueryString(controllerContext.HttpContext.Request.UrlReferrer.Query), CultureInfo.InvariantCulture)
{
}
}
public ActionResult GetReportSnapshot(MyReportModel model)
{
this.UpdateModel(model, new UrlReferrerValueProvider(ControllerContext));
return new EmptyResult();
}
I'm using MvvmCross to databind my ViewModel to an Android View layout.
From the SimpleBinding example I can see that to bind a value to a property I do this:
<EditText
android:hint="Subtotal"
android:gravity="left"
android:inputType="numberDecimal"
android:maxLines="1"
android:numeric="decimal"
local:MvxBind="{'Text':{'Path':'SubTotal','Converter':'Float'}}"
/>
so Text is bound to the SubTotal property of the ViewModel. But how do I bind to more than one property? In my case I want to bind a ViewModel property called HigherLower to the TextColor attribute of the layout element. I can't add another MvxBind and I can't set MvxBind to an array.
The format of the JSON used in the binding expression is a Dictionary of named MvxJsonBindingDescriptions
public class MvxJsonBindingDescription
{
public string Path { get; set; }
public string Converter { get; set; }
public string ConverterParameter { get; set; }
public string FallbackValue { get; set; }
public MvxBindingMode Mode { get; set; }
}
This is used with:
the dictionary Key name being the target (View) property for the binding.
the binding Path property being the source (DataContext) property for the binding - if Path is not specified then the whole DataContext itself is the binding source.
For Activity/View level axml the DataContext is the ViewModel - but for sub-View axml then the DataContext will normally be a child object of the ViewModel - e.g. inside a ListView the DataContext might be an item inside a List or ObservableCollection owned by the ViewModel.
To specify multiple bindings you can use JSON like:
{
'TargetProperty1':{'Path':'SourceProperty1'},
'TargetProperty2':{'Path':'SourceProperty2'}
}
For your particular example this might be:
local:MvxBind="
{
'Text':{'Path':'SubTotal','Converter':'Float'},
'TextColor':{'Path':'HigherLower','Converter':'MyColorConverter'}
}"
where your ViewModel is something like:
public class MyViewModel : IMvxViewModel
{
public float SubTotal { get; set; }
public bool HigherLower { get; set; }
// more code here
}
and your converter is something like:
public class MyColorConverter : MvxBaseColorConverter
{
protected override MvxColor Convert(object value, object parameter, CultureInfo culture)
{
return ((bool)value) ? new MvxColor(255,0,0) : new MvxColor(0,255,0);
}
}
and where that converter is initialized during Setup - e.g. see how the properties of the Converters class are used in TwitterSearch
One sample that shows Multiple Bindings at work is BestSellers - see Click and Text bound in the list item https://github.com/slodge/MvvmCross/blob/master/Sample%20-%20BestSellers/BestSellers/BestSellers.Droid/Resources/Layout/ListItem_Category.axml
Path':'HigherLowerYou must do this:
local:MvxBind="{'Text':{'Path':'SubTotal','Converter':'Float'}, 'TextColor':{'Path':'HigherLower','Converter':'Color'}}"
Note the:
bind="{ 'Text':{xx}, 'Other':{yy} }"
Perhaps there is an easy solution for my problem but I simply cannot seem to find it. I have read lots of tutorials about Knockout so I get the basics but I ask this question because my entity-structure is a bit more complicated than a person with a name and a list of friends which may or may not be on Twitter (Video on Channel9: Helping you build dynamic JavaScript UIs with MVVM and ASP.NET). Here's my situation:
I have a class PersonnelClass with this basic structure:
[Serializable]
//The interface is for the implementation of 'Name' and 'Description'
public class PersonnelClass : IPersonnelClassOrPerson
{
public PersonnelClass() : this(Guid.NewGuid(), "", "") { }
public PersonnelClass(Guid id, String name, String description = null)
{
if (id == Guid.Empty) { throw new ArgumentNullException("id"); }
Id = id;
Name = name;
Description = description;
Properties = new PropertyCollection();
}
public Guid Id { get; private set; }
public String Name { get; set; }
public String Description { get; set; }
public PropertyCollection Properties { get; private set; }
}
The PropertyCollection class and associated AbstractProperty class look like this:
[Serializable]
public class PropertyCollection: List<AbstractProperty> { }
[Serializable]
public abstract class AbstractProperty: IEntity, IProperty
{
public AbstractProperty(String name, String description = null) : this(Guid.NewGuid(), name, description) { }
public AbstractProperty(Guid id, String name, String description = null)
{
if (id == Guid.Empty) { throw new ArgumentNullException("id"); }
if (String.IsNullOrEmpty(name)) { throw new ArgumentNullException("name"); }
Id = id;
Name = name;
Description = description;
}
public Guid Id { get; private set; }
public String Name { get; private set; }
public String Description { get; private set; }
}
In my Controller, I create an instance of a PersonnelClassViewModel that has this structure:
public class PersonnelClassViewModel
{
public PersonnelClass PersonnelClass { get; set; }
public List<AbstractProperty> Properties { get; set; }
}
I fill this viewmodel with a new PersonnelClass and two test-properties to pass to my View like this:
var properties = new List<AbstractProperty>
{
new TextProperty("prop1", "descr1"),
new TextProperty("prop2", "descr2")
//TextProperty is derived from AbstractProperty
};
var vm = new PersonnelClassViewModel { Properties = properties };
return View(vm);
I get everything in my View as I wanted. From the View I want to create a new PersonnelClass with a set of selected properties. I have the fields for Name and Description and to add the properties I have a ListBox with the properties that already exist (for demo-purpose they came from the controller now). Through a bit of Knockout JavaScript code I can select items from this list and populate an HTML select-control () with the selected properties to add to the PersonnelClass. This all works fine, until I want to build up an object to pass back to the Controller and create the PersonnelClass.
My question is: what Knockout JS code is needed to build up this object and pass it to the Controller by submitting the form and in my Controller how should I receive this object, meaning: what type of object should this be (PersonnelClass, PersonnelClassViewModel, ...) ?
If any more info/code is needed, please do ask. Thanks in advance!
Update after answer of 'B Z':
I followed a few more of Steven Sanderson's tutorials about this to be sure I understand this, especially the one you provided in your answer. Now I have following code in my View to start with:
var initialData = #Html.Raw(new JavaScriptSerializer().Serialize(Model));
var viewModel = {
personnelClassViewModel : ko.mapping.fromJS(initialData),
properties : personnelClassViewModel.Properties,
selectedProperties : ko.observableArray([]),
addedProperties : ko.observableArray([])
};
ko.applyBindings(viewModel);
The variable 'initialData' contains the values I expect it to have but then I get the following error:
Microsoft JScript runtime error: 'personnelClassViewModel' is undefined
I have no clue anymore. Can anyone help me fix this?
Steven Sanderson has an example of how to to work with variable length lists and knockoutjs
http://blog.stevensanderson.com/2010/07/12/editing-a-variable-length-list-knockout-style/
Having said that, I think your problem isn't so much on the knockout side and more on the how to databind the data correctly on the server side. In the link above, Steven uses a FromJson attribute to model bind which you may find useful...
HTH
DataAnnotations does not work with buddy class. The following code always validate true. Why ?
var isValid = Validator.TryValidateObject(new Customer(), Context, results, true);
and here is the buddy class.
public partial class Customer
{
public string Name { get; set; }
public int Age { get; set; }
}
[MetadataType(typeof(CustomerMetaData))]
public partial class Customer
{
public class CustomerMetaData
{
[Required(ErrorMessage = "You must supply a name for a customer.")]
public string Name { get; set; }
}
}
Here is another thread with same question., but no answer.
link text
I found the answer here: http://forums.silverlight.net/forums/p/149264/377212.aspx
MVC recognizes the MetaDataType attribute, but other projects do not. Before validating, you need to manually register the metadata class:
TypeDescriptor.AddProviderTransparent(
new AssociatedMetadataTypeTypeDescriptionProvider(typeof(Customer), typeof(CustomerMetadata)), typeof(Customer));
var isValid = Validator.TryValidateObject(new Customer(), context, results, true);
After some research I couldn't find any reason why TryValidateObject always return true if I use MetadataType (buddy class). But it works with the following code (xVal).
public static IEnumerable<ErrorInfo> GetErrors(object instance, string name)
{
var metadataAttrib = instance.GetType()
.GetCustomAttributes(typeof(MetadataTypeAttribute), true)
.OfType<MetadataTypeAttribute>().FirstOrDefault();
var buddyClassOrModelClass = metadataAttrib != null
? metadataAttrib.MetadataClassType
: instance.GetType();
var buddyClassProperties = TypeDescriptor.GetProperties(buddyClassOrModelClass)
.Cast<PropertyDescriptor>();
var modelClassProperties = TypeDescriptor.GetProperties(instance.GetType())
.Cast<PropertyDescriptor>();
var list = from buddyProp in buddyClassProperties
join modelProp in modelClassProperties on
buddyProp.Name equals modelProp.Name
from attribute in buddyProp.Attributes.OfType<ValidationAttribute>()
where !attribute.IsValid(modelProp.GetValue(instance))
select new ErrorInfo(
buddyProp.Name,
attribute.FormatErrorMessage(modelProp.Name),
instance);
if (name != null)
list = list.Where(x => x.PropertyName == name);
return list;
}
Although I did not test your code in .NET 4.0, in .NET 3.5 / Silverlight 3, your metadata class should look like this:
[MetadataType(typeof(Customer.CustomerMetaData))]
public partial class Customer
{
internal sealed class CustomerMetaData
{
private CustomerMetaData()
{
}
[Required(ErrorMessage = "You must supply a name for a customer.")]
public string Name;
}
}
There is an issue where the MetadataType attribute is not being recognized by the object context. While you can manually add the type descriptor before validation:
TypeDescriptor.AddProviderTransparent(
new AssociatedMetadataTypeTypeDescriptionProvider(typeof(Customer), typeof(CustomerMetaData)), typeof(Customer));
a more concise way to handle it would be to update the Entity Model .tt file, to add the following to each DTO:
Type currentType = MethodBase.GetCurrentMethod().DeclaringType;
object[] attributes = currentType.GetCustomAttributes(typeof(MetadataTypeAttribute),false);
if(attributes.Length > 0)
{
//MetadataType attribute found!
MetadataTypeAttribute metaDataAttribute = (MetadataTypeAttribute)attributes[0];
TypeDescriptor.AddProviderTransparent(
new AssociatedMetadataTypeTypeDescriptionProvider(
currentType, metaDataAttribute.MetadataClassType),currentType);
}
This will allow you to add the attributes to the partial classes:
[MetadataType(typeof(CustomerMetaData))]
public partial class Customer
{
}
public partial class CustomerMetaData
{
[Required]
public string CustomerName { get; set; }
}