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 {
}
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]
public string name { get; set; }
[SaveFilterValue]
public int? age { get; set; }
[SaveFilterValue]
public DateTime? currentdate { get; set; }
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.
- Uses reflection, although not really an issue with speed, the extension method only reads and writes reflecting once each
- Not strongly typed
- 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 {
}
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); }
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>();
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.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"]);
});
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);
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