Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Extensions and Reflections and Generics oh my!

0.00/5 (No votes)
5 Mar 2017 1  
Creating an extension class for View Models to save public properties using Generics and Delegates, replacing a reflection implementation

Introduction

Recently, I was working on an MVC project in which the client wanted to persist a form used to filter data to a data store so that the form could be recalled with any number of saved states. I chose to persist the data in a dictionary object which could be persisted in a Entity Attribute Value table. (See this for an previous example of use.)

Background - Extension Reflection and Generics

I created two extension methods, one which saves model properties to a Dictionary and the other which applies data from a Dictionary and overwrites properties of the model.

The extension method for getting the properties is called GetModelValues and the other method is ValuesToModel.

Both extensions are defined as a type of T where T is defined as an Interface. This is to ensure that by simply adding an interface to an existing Model class, only Model classes would have these extensions.

public interface IModelClass { //does not matter that this is empty, 
                               //simply used to delineate classes that have these 
}

public static Dictionary<string, string> GetModelValues<T>(this T obj) where T : IModelClass

public static void ValuesToModel<T>
(this T CopyObject, Dictionary<string, string> obj) where T : IModelClass

The Model I am persisting is the Model in MVC, it's used to not only bind visual components of a form but additionally items which might be in hidden fields which may be passed back as part of the form. To ensure that I am only persisting public properties which are of concern, I have created a special attribute and assigned the attribute to the properties.

[AttributeUsage(AttributeTargets.Property)]
class SaveFilterValue : Attribute { }

    class Modelclass : IModelClass 
    {
        [SaveFilterValue] //Attached as a property attribute
        public string name { get; set; }
        [SaveFilterValue]
        public int? age { get; set; }
        [SaveFilterValue]
        public  DateTime? currentdate { get; set; }
        //Not persisted to dictionary.
        public string something_not_persisted { get; set; } 
    }

This is how the properties to read and return the Dictionary are implemented using reflection, notice only looking for public properties with attribute of SaveFilterValue:

public static Dictionary<string, string> GetModelValues<T>(this T obj) where T : IModelClass 
{
    Dictionary<string, string> propertyValues = new Dictionary<string, string>();

    PropertyInfo[] Props = obj.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public).

    Where(w => w.CustomAttributes.FirstOrDefault
            (fd => fd.AttributeType == typeof(SaveFilterValue)) !=null).ToArray();
    foreach (PropertyInfo Prop in Props){
        if (!propertyValues.ContainsKey(Prop.Name) && Prop.GetValue(obj, null) != null){
            propertyValues.Add(Prop.Name, Prop.GetValue(obj, null).ToString());}}
    return propertyValues;
}

And Now Delegates

As much as this works and works well, it has its downsides.

  1. Uses reflection, although not really an issue with speed, the extension method only reads and writes reflecting once each
  2. Not strongly typed
  3. Need to modify the MVC Model for each property you wish to persist

I re-wrote these two extension methods as Delegates to improve on some of these downsides.

For more detail on Delegates, go to my article.

public interface IModelClass { //does not matter that this is empty, 
                               //simply used to delineate classes that have these 
}

public static Dictionary<string, string> GetModelValues<T>
(this T obj, Func<T, Dictionary<string, string>> func) where T : IModelClass 
       
public static void ValuesToModel<T>(this T CopyObject, Action<T> func) where T : IModelClass 

Because the actual work is being done by a delegate, the implementation of these two methods literally consist of one line each and no longer require you to add an attribute on the property.

public static void ValuesToModel<T>(this T CopyObject, Action<T> func) where T : IModelClass 
{ func.Invoke(CopyObject); }

public static Dictionary<string, string> GetModelValues<T>
(this T obj, Func<T, Dictionary<string, string>> func) where T : IModelClass 
{ return func.Invoke(obj); }

//There is an override to accept the Dictionary object as a parameter 
//as well as using in the scope of the delegate

public static void ValuesToModel<T>(this T CopyObject, Action<T, Dictionary<string, string>> func, 
Dictionary<string, string> DictionaryValues) where T : IModelClass 
{ func.Invoke(CopyObject, DictionaryValues); }

Using the Code

To use the extension methods, you need to instantiate a class which implements the IModelClass and then supply an anonymous function to the extension methods which does the heavy lifting.

Modelclass ViewModel = new Modelclass() { name = "Steve", age = null, currentdate = DateTime.Now };

Dictionary<string, string> values = ViewModel.GetModelValues(p => {
     Dictionary<string, string> temp = new Dictionary<string, string>();
        //Test to see if a value exists and add it to the dictionary
        if (!string.IsNullOrEmpty(p.name))
            temp.Add("name", p.name);
        if (p.currentdate.HasValue)
            temp.Add("currentdate", p.currentdate.ToString());
        if (p.age.HasValue)
            temp.Add("age", p.age.ToString());
        return temp;
});
//values has two items name and currentdate
//Let's add a new item to the dictionary and change the name and update the object.
values.Add("age", "12");
values["name"] = "Bob";

ViewModel.ValuesToModel((p) => {
      if(values.Keys.Contains("name"))
            p.name = values["name"];
      if (values.Keys.Contains("currentdate"))
            p.currentdate = DateTime.Parse(values["currentdate"]);
      if (values.Keys.Contains("age"))
            p.age = int.Parse(values["age"]);
    });
//values has three items name(Bob) Age(12) and currentdate

One thing to note is using reflection there where no string constants required. We used the .name method for the key and .getvalue method for the value. This code was written using Visual Studio 2013. Using Visual Studio 2015 and the latest framework allows you to use a new keyword called nameof() which will return the name of the property and eliminate the need for the string contants.

Dictionary<string, string> pass_this_in = new Dictionary<string, string>();
pass_this_in.Add("name", "Pass this in");

ViewModel2015.ValuesToModel((deleg, dict) =>
            {
                if (dict.Keys.Contains(nameof(deleg.name)))
                    deleg.name = dict[nameof(deleg.name)];
                if (dict.Keys.Contains(nameof(deleg.currentdate)))
                    deleg.currentdate = DateTime.Parse(dict[nameof(deleg.currentdate)]);
                if (dict.Keys.Contains(nameof(deleg.age)))
                    deleg.age = int.Parse(dict[nameof(deleg.age)]);

            }, pass_this_in);

//ViewModel2015.name will equal "Pass this in"

The reference code attached will contain examples of all the code presented here. Note that some of it will only compile in 2015 and others can compile in older versions as well. I have even included an extra credit section for anyone to try. :)

Points of Interest

Googling how to do things like this is like going down the yellow brick road (2nd reference to The Wizard of Oz), easy to follow and very instructive. But where is the personal growth in that? Be careful to examine the solutions and use if appropriate but don't assume that someone else knows all the answers. Strive to do things a little differently. Don't be scared to change things or refactor them to make things more clear. Just be careful (add test cases) and keep on learning!

History

Added link to source code

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here