/* * Munger - An Interface pattern on getting and setting values from object through Reflection * * Author: Phillip Piper * Date: 28/11/2008 17:15 * * Change log: * v2.5.1 * 2012-05-01 JPP - Added IgnoreMissingAspects property * v2.5 * 2011-05-20 JPP - Accessing through an indexer when the target had both a integer and * a string indexer didn't work reliably. * v2.4.1 * 2010-08-10 JPP - Refactored into Munger/SimpleMunger. 3x faster! * v2.3 * 2009-02-15 JPP - Made Munger a public class * 2009-01-20 JPP - Made the Munger capable of handling indexed access. * Incidentally, this removed the ugliness that the last change introduced. * 2009-01-18 JPP - Handle target objects from a DataListView (normally DataRowViews) * v2.0 * 2008-11-28 JPP Initial version * * TO DO: * * Copyright (C) 2006-2014 Phillip Piper * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * If you wish to use this code in a closed source application, please contact phillip_piper@bigfoot.com. */ using System; using System.Collections.Generic; using System.Reflection; namespace BrightIdeasSoftware { /// /// An instance of Munger gets a value from or puts a value into a target object. The property /// to be peeked (or poked) is determined from a string. The peeking or poking is done using reflection. /// /// /// Name of the aspect to be peeked can be a field, property or parameterless method. The name of an /// aspect to poke can be a field, writable property or single parameter method. /// /// Aspect names can be dotted to chain a series of references. /// /// Order.Customer.HomeAddress.State /// public class Munger { #region Life and death /// /// Create a do nothing Munger /// public Munger() { } /// /// Create a Munger that works on the given aspect name /// /// The name of the public Munger(String aspectName) { this.AspectName = aspectName; } #endregion #region Static utility methods /// /// A helper method to put the given value into the given aspect of the given object. /// /// This method catches and silently ignores any errors that occur /// while modifying the target object /// The object to be modified /// The name of the property/field to be modified /// The value to be assigned /// Did the modification work? public static bool PutProperty(object target, string propertyName, object value) { try { Munger munger = new Munger(propertyName); return munger.PutValue(target, value); } catch (MungerException) { // Not a lot we can do about this. Something went wrong in the bowels // of the property. Let's take the ostrich approach and just ignore it :-) // Normally, we would never just silently ignore an exception. // However, in this case, this is a utility method that explicitly // contracts to catch and ignore errors. If this is not acceptible, // the programmer should not use this method. } return false; } /// /// Gets or sets whether Mungers will silently ignore missing aspect errors. /// /// /// /// By default, if a Munger is asked to fetch a field/property/method /// that does not exist from a model, it returns an error message, since that /// condition is normally a programming error. There are some use cases where /// this is not an error, and the munger should simply keep quiet. /// /// By default this is true during release builds. /// public static bool IgnoreMissingAspects { get { return ignoreMissingAspects; } set { ignoreMissingAspects = value; } } private static bool ignoreMissingAspects #if !DEBUG = true #endif ; #endregion #region Public properties /// /// The name of the aspect that is to be peeked or poked. /// /// /// /// This name can be a field, property or parameter-less method. /// /// /// The name can be dotted, which chains references. If any link in the chain returns /// null, the entire chain is considered to return null. /// /// /// "DateOfBirth" /// "Owner.HomeAddress.Postcode" public string AspectName { get { return aspectName; } set { aspectName = value; // Clear any cache aspectParts = null; } } private string aspectName; #endregion #region Public interface /// /// Extract the value indicated by our AspectName from the given target. /// /// If the aspect name is null or empty, this will return null. /// The object that will be peeked /// The value read from the target public Object GetValue(Object target) { if (this.Parts.Count == 0) return null; try { return this.EvaluateParts(target, this.Parts); } catch (MungerException ex) { if (Munger.IgnoreMissingAspects) return null; return String.Format("'{0}' is not a parameter-less method, property or field of type '{1}'", ex.Munger.AspectName, ex.Target.GetType()); } } /// /// Extract the value indicated by our AspectName from the given target, raising exceptions /// if the munger fails. /// /// If the aspect name is null or empty, this will return null. /// The object that will be peeked /// The value read from the target public Object GetValueEx(Object target) { if (this.Parts.Count == 0) return null; return this.EvaluateParts(target, this.Parts); } /// /// Poke the given value into the given target indicated by our AspectName. /// /// /// /// If the AspectName is a dotted path, all the selectors bar the last /// are used to find the object that should be updated, and the last /// selector is used as the property to update on that object. /// /// /// So, if 'target' is a Person and the AspectName is "HomeAddress.Postcode", /// this method will first fetch "HomeAddress" property, and then try to set the /// "Postcode" property on the home address object. /// /// /// The object that will be poked /// The value that will be poked into the target /// bool indicating whether the put worked public bool PutValue(Object target, Object value) { if (this.Parts.Count == 0) return false; SimpleMunger lastPart = this.Parts[this.Parts.Count - 1]; if (this.Parts.Count > 1) { List parts = new List(this.Parts); parts.RemoveAt(parts.Count - 1); try { target = this.EvaluateParts(target, parts); } catch (MungerException ex) { this.ReportPutValueException(ex); return false; } } if (target != null) { try { return lastPart.PutValue(target, value); } catch (MungerException ex) { this.ReportPutValueException(ex); } } return false; } #endregion #region Implementation /// /// Gets the list of SimpleMungers that match our AspectName /// private IList Parts { get { if (aspectParts == null) aspectParts = BuildParts(this.AspectName); return aspectParts; } } private IList aspectParts; /// /// Convert a possibly dotted AspectName into a list of SimpleMungers /// /// /// private IList BuildParts(string aspect) { List parts = new List(); if (!String.IsNullOrEmpty(aspect)) { foreach (string part in aspect.Split('.')) { parts.Add(new SimpleMunger(part.Trim())); } } return parts; } /// /// Evaluate the given chain of SimpleMungers against an initial target. /// /// /// /// private object EvaluateParts(object target, IList parts) { foreach (SimpleMunger part in parts) { if (target == null) break; target = part.GetValue(target); } return target; } private void ReportPutValueException(MungerException ex) { //TODO: How should we report this error? System.Diagnostics.Debug.WriteLine("PutValue failed"); System.Diagnostics.Debug.WriteLine(String.Format("- Culprit aspect: {0}", ex.Munger.AspectName)); System.Diagnostics.Debug.WriteLine(String.Format("- Target: {0} of type {1}", ex.Target, ex.Target.GetType())); System.Diagnostics.Debug.WriteLine(String.Format("- Inner exception: {0}", ex.InnerException)); } #endregion } /// /// A SimpleMunger deals with a single property/field/method on its target. /// /// /// Munger uses a chain of these resolve a dotted aspect name. /// public class SimpleMunger { #region Life and death /// /// Create a SimpleMunger /// /// public SimpleMunger(String aspectName) { this.aspectName = aspectName; } #endregion #region Public properties /// /// The name of the aspect that is to be peeked or poked. /// /// /// /// This name can be a field, property or method. /// When using a method to get a value, the method must be parameter-less. /// When using a method to set a value, the method must accept 1 parameter. /// /// /// It cannot be a dotted name. /// /// public string AspectName { get { return aspectName; } } private readonly string aspectName; #endregion #region Public interface /// /// Get a value from the given target /// /// /// public Object GetValue(Object target) { if (target == null) return null; this.ResolveName(target, this.AspectName, 0); try { if (this.resolvedPropertyInfo != null) return this.resolvedPropertyInfo.GetValue(target, null); if (this.resolvedMethodInfo != null) return this.resolvedMethodInfo.Invoke(target, null); if (this.resolvedFieldInfo != null) return this.resolvedFieldInfo.GetValue(target); // If that didn't work, try to use the indexer property. // This covers things like dictionaries and DataRows. if (this.indexerPropertyInfo != null) return this.indexerPropertyInfo.GetValue(target, new object[] { this.AspectName }); } catch (Exception ex) { // Lots of things can do wrong in these invocations throw new MungerException(this, target, ex); } // If we get to here, we couldn't find a match for the aspect throw new MungerException(this, target, new MissingMethodException()); } /// /// Poke the given value into the given target indicated by our AspectName. /// /// The object that will be poked /// The value that will be poked into the target /// bool indicating if the put worked public bool PutValue(object target, object value) { if (target == null) return false; this.ResolveName(target, this.AspectName, 1); try { if (this.resolvedPropertyInfo != null) { this.resolvedPropertyInfo.SetValue(target, value, null); return true; } if (this.resolvedMethodInfo != null) { this.resolvedMethodInfo.Invoke(target, new object[] { value }); return true; } if (this.resolvedFieldInfo != null) { this.resolvedFieldInfo.SetValue(target, value); return true; } // If that didn't work, try to use the indexer property. // This covers things like dictionaries and DataRows. if (this.indexerPropertyInfo != null) { this.indexerPropertyInfo.SetValue(target, value, new object[] { this.AspectName }); return true; } } catch (Exception ex) { // Lots of things can do wrong in these invocations throw new MungerException(this, target, ex); } return false; } #endregion #region Implementation private void ResolveName(object target, string name, int numberMethodParameters) { if (cachedTargetType == target.GetType() && cachedName == name && cachedNumberParameters == numberMethodParameters) return; cachedTargetType = target.GetType(); cachedName = name; cachedNumberParameters = numberMethodParameters; resolvedFieldInfo = null; resolvedPropertyInfo = null; resolvedMethodInfo = null; indexerPropertyInfo = null; const BindingFlags flags = BindingFlags.Public | BindingFlags.Instance /*| BindingFlags.NonPublic*/; foreach (PropertyInfo pinfo in target.GetType().GetProperties(flags)) { if (pinfo.Name == name) { resolvedPropertyInfo = pinfo; return; } // See if we can find an string indexer property while we are here. // We also need to allow for old style keyed collections. if (indexerPropertyInfo == null && pinfo.Name == "Item") { ParameterInfo[] par = pinfo.GetGetMethod().GetParameters(); if (par.Length > 0) { Type parameterType = par[0].ParameterType; if (parameterType == typeof(string) || parameterType == typeof(object)) indexerPropertyInfo = pinfo; } } } foreach (FieldInfo info in target.GetType().GetFields(flags)) { if (info.Name == name) { resolvedFieldInfo = info; return; } } foreach (MethodInfo info in target.GetType().GetMethods(flags)) { if (info.Name == name && info.GetParameters().Length == numberMethodParameters) { resolvedMethodInfo = info; return; } } } private Type cachedTargetType; private string cachedName; private int cachedNumberParameters; private FieldInfo resolvedFieldInfo; private PropertyInfo resolvedPropertyInfo; private MethodInfo resolvedMethodInfo; private PropertyInfo indexerPropertyInfo; #endregion } /// /// These exceptions are raised when a munger finds something it cannot process /// public class MungerException : ApplicationException { /// /// Create a MungerException /// /// /// /// public MungerException(SimpleMunger munger, object target, Exception ex) : base("Munger failed", ex) { this.munger = munger; this.target = target; } /// /// Get the munger that raised the exception /// public SimpleMunger Munger { get { return munger; } } private readonly SimpleMunger munger; /// /// Gets the target that threw the exception /// public object Target { get { return target; } } private readonly object target; } /* * We don't currently need this * 2010-08-06 * internal class SimpleBinder : Binder { public override FieldInfo BindToField(BindingFlags bindingAttr, FieldInfo[] match, object value, System.Globalization.CultureInfo culture) { //return Type.DefaultBinder.BindToField( throw new NotImplementedException(); } public override object ChangeType(object value, Type type, System.Globalization.CultureInfo culture) { throw new NotImplementedException(); } public override MethodBase BindToMethod(BindingFlags bindingAttr, MethodBase[] match, ref object[] args, ParameterModifier[] modifiers, System.Globalization.CultureInfo culture, string[] names, out object state) { throw new NotImplementedException(); } public override void ReorderArgumentArray(ref object[] args, object state) { throw new NotImplementedException(); } public override MethodBase SelectMethod(BindingFlags bindingAttr, MethodBase[] match, Type[] types, ParameterModifier[] modifiers) { throw new NotImplementedException(); } public override PropertyInfo SelectProperty(BindingFlags bindingAttr, PropertyInfo[] match, Type returnType, Type[] indexes, ParameterModifier[] modifiers) { if (match == null) throw new ArgumentNullException("match"); if (match.Length == 0) return null; return match[0]; } } */ }