/*
* 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