1

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

elector
  • 1,327
  • 4
  • 26
  • 43
  • Yes, of course. Why wouldn't you be able to? – McGarnagle Oct 29 '13 at 20:02
  • Thanks :) any references on how to? – elector Oct 29 '13 at 20:21
  • Speaking for myself, I don't have time to work through the whole problem. (And the question only has 9 views so far :P). If you make an attempt/start I might be able to help. – McGarnagle Oct 29 '13 at 20:46
  • I still have no clue where to start on this? I would very much appreciate someone pointing me to a page or anything to get me started. – elector Nov 01 '13 at 21:28
  • But you haven't indicated what the problem is ... this isn't a site where you can give people specs and have them do the work for free. Do you not know how to write a custom annotation? How to retrieve it using reflection? How to do so specifically inside a T4 template? What? – McGarnagle Nov 01 '13 at 21:31
  • Ok, now I get something. Thank you. I don't expect anyone to solve this for me. I thought someone has already done this and knows about a blog post that describes it. I know how to make a custom annotation attribute. But getting it out in T4 template with reflection I don't. I am looking at the code at the bottom of the List.tt and I don't understand it. And I can't find any reference on the internet that describes it. If you, or anyone else, doesn't know of any I'll just leave it at that. – elector Nov 01 '13 at 21:51

3 Answers3

5

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;
         }
     }
}
Korayem
  • 12,108
  • 5
  • 69
  • 56
Johnie Karr
  • 2,744
  • 2
  • 35
  • 44
  • It is very cumbersome :( You should do both: reference new assembly from contract/model assembly (what you would not like to do if you want to keep references "simple" ) and add reference the same assembly from T4 template (what could make more complex developer environment setup)... – Roman Pokrovskij Jul 27 '15 at 01:14
  • would love to see an update on your blog and simplified way of getting the datatypes inside the VIew Templates, can not believe MS took it out!! – Transformer Feb 16 '17 at 06:55
4

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.

elector
  • 1,327
  • 4
  • 26
  • 43
  • 1
    This looks like exactly what I need, except I am using MVC 5 and the templates seem completely different (can't find any of the locations in the template you mention). Do you know how to do it in MVC 5? – Nick Thissen May 07 '14 at 14:36
  • 1
    I haven't tried the MVC5 templates yet. I don't think I will do this kind of thing for the MVC 5 in the near future. But, please, if you do find a way, post it here. Eventually this sort of thing should be in a blog post, right? – elector May 07 '14 at 20:25
  • @NickThissen, did you ever find a solution to this in MVC 5? – Johnie Karr May 15 '15 at 18:49
  • I could not remember but it seems I did as I have some code that does this. I've posted it as an additional answer. – Nick Thissen May 15 '15 at 23:06
2

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.

Nick Thissen
  • 1,802
  • 4
  • 27
  • 38