-->
These old forums are deprecated now and set to read-only. We are waiting for you on our new forums!
More modern, Discourse-based and with GitHub/Google/Twitter authentication built-in.

All times are UTC - 5 hours [ DST ]



Forum locked This topic is locked, you cannot edit posts or make further replies.  [ 14 posts ] 
Author Message
 Post subject: How to detect whether object has changes or not
PostPosted: Fri Feb 22, 2008 7:32 am 
Newbie

Joined: Wed Feb 20, 2008 9:15 pm
Posts: 11
Hello,
what is best practice to detect if an object is modified to display a message whether save or not the changes?

Does NHibernate offer such functionality?
Especially with collections I cannot simply modify the setter to set a modified flag.

Any recommendation?


Top
 Profile  
 
 Post subject:
PostPosted: Fri Feb 22, 2008 11:57 am 
Expert
Expert

Joined: Fri Oct 28, 2005 5:38 pm
Posts: 390
Location: Cedarburg, WI
NHibernate was designed to work without imposing any requirements on your entity classes. It does its own internal tracking in the session for whether an entity being tracked by the session is dirty. While it would be nice if it exposed some method on ISession to check if an entity is dirty, that doesn't address changes made to entities while they are detached from a session.

We implemented our own mechanism for entities to track not only if they have unsaved changes, but what the changes are. If you need to know when an entity is dirty, you'll need to do something similar. If you'd like code examples on how to do it, just let me know.


Top
 Profile  
 
 Post subject:
PostPosted: Fri Feb 22, 2008 2:23 pm 
Newbie

Joined: Wed Feb 20, 2008 9:15 pm
Posts: 11
Hello,
an example would be nice.

My suggestion is a "IsDirty" Property for each class. Is there a way to "simple" let hibernate create a "IsDirty = true" in every setter?


Top
 Profile  
 
 Post subject:
PostPosted: Tue Feb 26, 2008 10:52 am 
Newbie

Joined: Tue Feb 26, 2008 10:42 am
Posts: 5
Location: England
Your class could implement ICloneable and IEquatable, then you could make a clone of your object before you allow the user to make changes, then afterwards compare that clone to the current object to see if they are no longer the same.

If the collections within your class also implement ICloneable and IEquatable, this method could also let you know if something changed somewhere down the hierarchy.


Top
 Profile  
 
 Post subject:
PostPosted: Tue Feb 26, 2008 4:09 pm 
Expert
Expert

Joined: Fri Oct 28, 2005 5:38 pm
Posts: 390
Location: Cedarburg, WI
Again, NHibernate is specifically designed to make no requirements on your entity classes (other than the fact that they need an identifier property). That means you have to implement your own change tracking if you need it.

Here is pared down code that we use to track changes to an entity. Note that we keep two sets of old values; one for IEditableObject support and one for all changes since the entity was last persisted. We expose the unpersisted changes during events we raise from our implementation of IInterceptor before the entity gets flushed and committed.

Note that this code example does not deal with changes to collections. All of our collections are inverse=true, so adding or removing a collection item will not change the underlying parent database row. For that reason we do not consider adding or removing an item on a collection property to be a change on the entity.

Also note that the base entity class has IsTransient and IsDeleted properties. IsTransient gets set by our entity factory and cleared by our IInterceptor.PostFlush implementation. IsDeleted gets set by the entity base class's MarkForDeletion() method, and it is cleared (and IsTransient set) by IInterceptor.PostFlush. These properties are all set and cleared by reflection because we do not want application-level code messing with them.

Code:
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Reflection;

using MyCompany.MyProduct.Common;

namespace MyCompany.MyProduct.Entities
{
    [Serializable]
    public class SomeEntity :
        EntityBase
    {
        private object _syncRoot = new object();
        private string _id;
        ...

        public override string Id
        {
            get {return this._id ?? string.Empty;}
            set
            {
                lock (this._syncRoot)
                {
                    if (value == null) value = String.Empty;
                    bool shouldValidate = (!this.EditHelper.InOriginalValuesReset);
                    if (shouldValidate)
                    {
                        // do validation
                    }
                    if (this._id != value)
                    {
                        string oldValue = this._id;
                        // Raise any PropertyChanging event here; bail if e.Cancel = true
                        this.EditHelper.Edit("Id", oldValue);
                        this._id = value;
                        this.IsChanged = true;
                        this.UnpersistedChangeTracker.Edit("Id", oldValue);
                        // Raise any PropertyChanged event here
                    }
                }
            }
        }

        ... // Other properties

        public SomeEntity() : base()
        {
            this._id = null;
            ... // Other properties
        }
    }

    [Serializable]
    public abstract class EntityBase :
        IDecoupledEditableObject
    {
        #region Fields

        private object _syncRoot = new object();
        private bool? _wasChangedBeforeEdit;
        private bool _isChanged;
        private bool _isDeleted;
        private bool _isTransient;
        private EditableObjectHelper _editHelper;
        private EditableObjectHelper _unpersistedChangeTracker;

        #endregion

        #region Properties

        /// <summary>
        /// Indicates that one of more of this entity's values have changed
        /// since it was last persisted.
        /// </summary>
        [Description("Indicates that one of more of this entity's values have changed since it was last persisted.")]
        [Category("Persistence")]
        public virtual bool IsChanged
        {
            get { return this._isChanged; }
            protected set
            {
                lock (this._syncRoot)
                {
                    if (this._isChanged != value)
                    {
                        this._isChanged = value;

                        if (this._isChanged)
                        {
                            this.UnpersistedChangeTracker.BeginEdit();
                        }
                        else
                        {
                            this.UnpersistedChangeTracker.EndEdit();
                        }
                    }
                }
            }
        }

        /// <summary>
        /// Indicates that this entity is marked for permanent removal
        /// from the persist site.
        /// </summary>
        [Description("Indicates that this entity is marked for permanent removal from the persist site.")]
        [Category("Persistence")]
        public virtual bool IsDeleted
        {
            get { return this._isDeleted; }
            protected set { this._isDeleted = value; }
        }

        /// <summary>
        /// Indicates that this entity is 'new', i.e. has not been persisted.
        /// </summary>
        [Description("Indicates that this entity is 'new', i.e. has not been persisted.")]
        [Category("Persistence")]
        public virtual bool IsTransient
        {
            get { return this._isTransient; }
            protected set { this._isTransient = value; }
        }

        /// <summary>
        /// The entity's 'primary key'.
        /// </summary>
        [Description("The entity's 'primary key'.")]
        [Category("Identification")]
        public abstract string Id
        {
            get;
            set;
        }

        /// <summary>
        /// A helper to implement IEditableObject.
        /// </summary>
        [Description("A helper to implement IEditableObject.")]
        [Browsable(false)]
        protected EditableObjectHelper EditHelper
        {
            get
            {
                if (this._editHelper == null)
                {
                    lock (this._syncRoot)
                    {
                        if (this._editHelper == null)
                        {
                            this._editHelper =
                                new EditableObjectHelper(
                                    this, new EditingEventHandler(this.RaiseEditing));
                        }
                    }
                }
                return this._editHelper;
            }
        }

        /// <summary>
        /// A helper to track changes since the entity was last persisted.
        /// </summary>
        [Description("A helper to track changes since the entity was last persisted.")]
        [Browsable(false)]
        protected EditableObjectHelper UnpersistedChangeTracker
        {
            get
            {
                if (this._unpersistedChangeTracker == null)
                {
                    lock (this._syncRoot)
                    {
                        if (this._unpersistedChangeTracker == null)
                        {
                            this._unpersistedChangeTracker =
                                new EditableObjectHelper(this, null);
                        }
                    }
                }
                return this._unpersistedChangeTracker;
            }
        }

        #endregion

        #region Constructors

        public EntityBase()
        {
            this._isChanged = false;
            this._isDeleted = false;
            this._editHelper = null;
            this._unpersistedChangeTracker = null;
        }

        #endregion

        #region Methods

        /// <summary>
        /// A local implementation of Equals based on primary key values.
        /// </summary>
        public override bool Equals(object obj)
        {
            if (this == obj)
                return true;

            if ((obj == null) || !this.GetType().IsAssignableFrom(obj.GetType()))
                return false;

            EntityBase that = (EntityBase)obj;

            string thisId = this.Id.TrimEnd();
            string thatId = that.Id.TrimEnd();

            return
                (!string.IsNullOrEmpty(thisId) &&
                 !string.IsNullOrEmpty(thatId) &&
                 thisId == thatId);
        }

        /// <summary>
        /// A local implementation of GetHashCode based on primary key values.
        /// </summary>
        public override int GetHashCode()
        {
            int hash = 57;

            if (!string.IsNullOrEmpty(this.Id.TrimEnd()))
                hash *= 29 * this.Id.TrimEnd().GetHashCode();

            return hash;
        }

        #endregion // Equals And GetHashCode Overrides

        #region Public Methods

        /// <summary>
        /// Mark the entity for permanent removal from the persist site.
        /// </summary>
        public void MarkForDeletion()
        {
            this._isDeleted = true;
            this._isChanged = true;
        }

        #endregion

        #region IDecoupledEditableObject members

        /// <summary>
        /// Raised before and after this object begins, cancels and ends editing.
        /// </summary>
        public event EditingEventHandler Editing;

        /// <summary>
        /// Gets whether or not <see cref="IEditableObject.BeginEdit"/> has been called
        /// since the last call to <see cref="IEditableObject.CancelEdit"/>
        /// or <see cref="IEditableObject.EndEdit"/>.
        /// </summary>
        [Browsable(false)]
        public bool InEdit
        {
            get { return (this._editHelper != null ? this._editHelper.InEdit : false); }
        }

        /// <summary>
        /// Begins an edit on an object.
        /// </summary>
        public void BeginEdit()
        {
            lock (this._syncRoot)
            {
                if (this.InEdit)
                    return;

                if (!this._wasChangedBeforeEdit.HasValue)
                    this._wasChangedBeforeEdit = this._isChanged;

                this.EditHelper.BeginEdit();
            }
        }

        /// <summary>
        /// Discards changes since the last <see cref="BeginEdit"/> call.
        /// </summary>
        public void CancelEdit()
        {
            lock (this._syncRoot)
            {
                if (!this.InEdit)
                    return;

                this.EditHelper.CancelEdit();

                this._isChanged = this._wasChangedBeforeEdit.Value;
                this._wasChangedBeforeEdit = null;
            }
        }

        /// <summary>
        /// Pushes changes since the last <see cref="BeginEdit"/>
        /// or <see cref="IBindingList.AddNew"/> call into the underlying object.
        /// </summary>
        public void EndEdit()
        {
            lock (this._syncRoot)
            {
                if (!this.InEdit)
                    return;

                this.EditHelper.EndEdit();

                this._wasChangedBeforeEdit = null;
            }
        }

        /// <summary>
        /// Raises the Editing event.
        /// </summary>
        protected void RaiseEditing(object sender, EditingEventArgs e)
        {
            if (this.Editing != null)
                this.Editing(sender, e);
        }

        #endregion // IDecoupledEditableObject members
    }
}

namespace MyCompany.MyProduct.Common
{
    /// <summary>
    /// Allows classes to implement <see cref="IEditableObject"/>
    /// without being coupled to their containers.
    /// </summary>
    /// <remarks>
    /// The <see cref="Editing"/> event allows the developer
    /// to keep the responsibility of adding an item created with
    /// <see cref="IBindingList.AddNew">IBindingList.AddNew</see> within the list
    /// implementation itself, and out of the <see cref="IEditableObject"/>'s
    /// implementation of <see cref="IEditableObject.EndEdit"/>.
    /// /// </remarks>
    public interface IDecoupledEditableObject : IEditableObject
    {
        /// <summary>
        /// Raised before and after this object begins, cancels and ends editing.
        /// </summary>
        event EditingEventHandler Editing;

        /// <summary>
        /// Gets whether or not <see cref="IEditableObject.BeginEdit"/> has been called
        /// since the last call to <see cref="IEditableObject.CancelEdit"/>
        /// or <see cref="IEditableObject.EndEdit"/>.
        /// </summary>
        bool InEdit { get;}
    }

    public delegate void EditingEventHandler(object sender, EditingEventArgs e);

    public class EditingEventArgs : EventArgs
    {
        private IEditableObject      _editedObject;
        private IDictionary          _originalValues;
        private ItemChangeTiming     _timing;
        private EditableObjectAction _action;
        private bool                 _cancel;

        public IEditableObject EditedObject
        {
            get { return this._editedObject; }
        }

        public IDictionary OriginalValues
        {
            get { return this._originalValues; }
        }

        public ItemChangeTiming Timing
        {
            get { return this._timing; }
        }

        public EditableObjectAction Action
        {
            get { return this._action; }
        }

        public bool Cancel
        {
            get { return this._cancel; }
            set { this._cancel = value; }
        }

        public EditingEventArgs(
            IEditableObject      editedObject,
            ItemChangeTiming     timing,
            EditableObjectAction action)
            : this(editedObject, null, timing, action)
        {
        }

        public EditingEventArgs(
            IEditableObject      editedObject,
            IDictionary          originalValues,
            ItemChangeTiming     timing,
            EditableObjectAction action)
        {
            this._editedObject = editedObject;
            this._timing = timing;
            this._action = action;

            this._originalValues =
                (originalValues != null
                ? originalValues
                : new Hashtable());
        }
    }

    public enum EditableObjectAction
    {
        BeginEdit,
        CancelEdit,
        EndEdit
    }

    public enum ItemChangeTiming
    {
        Before,
        After
    }

    [Serializable]
    public class EditableObjectHelper
    {
        private object _syncRoot = new object();
        private IEditableObject _master;
        private IDictionary _originalValues;
        private bool _inEdit;
        private bool _inOriginalValuesReset;
        private EditingEventHandler _raiseEditingHandler;

        public bool InEdit
        {
            get { return this._inEdit; }
        }

        public bool InOriginalValuesReset
        {
            get { return this._inOriginalValuesReset; }
        }

        [Browsable(false)]
        protected IDictionary OriginalValues
        {
            get
            {
                if (this._originalValues == null)
                {
                    this._originalValues = new Hashtable();
                }
                return this._originalValues;
            }
        }

        public EditableObjectHelper(
            IEditableObject master)
            :
            this(master, null)
        {
        }

        public EditableObjectHelper(
            IEditableObject master,
            EditingEventHandler raiseEditingHandler)
        {
            if (master != null)
                throw new ArgumentNullException("master");

            this._master = master;
            this._raiseEditingHandler = raiseEditingHandler;
        }

        public void BeginEdit()
        {
            lock (this._syncRoot)
            {
                if (this._inEdit)
                    return;

                if (this._raiseEditingHandler != null)
                {
                    EditingEventArgs e = new EditingEventArgs(
                        this._master,
                        ItemChangeTiming.Before,
                        EditableObjectAction.BeginEdit);

                    this._raiseEditingHandler(this._master, e);
                    if (e.Cancel)
                        return;
                }

                this._inEdit = true;

                if (this._raiseEditingHandler != null)
                {
                    this._raiseEditingHandler(this._master,
                        new EditingEventArgs(
                            this._master,
                            ItemChangeTiming.After,
                            EditableObjectAction.BeginEdit));
                }
            }
        }

        public void Edit(string propertyName, object value)
        {
            lock (this._syncRoot)
            {
                if (this._inOriginalValuesReset)
                    return;

                if (this._inEdit)
                {
                    IDictionary originalValues = this.OriginalValues;
                    if (!originalValues.Contains(propertyName))
                    {
                        originalValues.Add(
                            propertyName,
                            value);
                    }
                }
            }
        }

        public void CancelEdit()
        {
            lock (this._syncRoot)
            {
                if (!this._inEdit)
                    return;

                if (this._raiseEditingHandler != null)
                {
                    EditingEventArgs e = new EditingEventArgs(
                        this._master,
                        this.OriginalValues,
                        ItemChangeTiming.Before,
                        EditableObjectAction.CancelEdit);

                    this._raiseEditingHandler(this._master, e);
                    if (e.Cancel)
                        return;
                }

                try
                {
                    this._inOriginalValuesReset = true;

                    foreach (DictionaryEntry entry in originalValues)
                    {
                        ReflectionHelper.SetPropertyValue(
                            this._master, (string)entry.Key, entry.Value);
                    }
                }
                finally
                {
                    this._inOriginalValuesReset = false;
                }

                this._inEdit = false;

                if (this._raiseEditingHandler != null)
                {
                    this._raiseEditingHandler(this._master,
                        new EditingEventArgs(
                            this._master,
                            this.OriginalValues,
                            ItemChangeTiming.After,
                            EditableObjectAction.CancelEdit));
                }

                this.OriginalValues.Clear();
            }
        }

        public void EndEdit()
        {
            lock (this._syncRoot)
            {
                if (!this._inEdit)
                    return;

                if (this._raiseEditingHandler != null)
                {
                    EditingEventArgs e = new EditingEventArgs(
                        this._master,
                        this.OriginalValues,
                        ItemChangeTiming.Before,
                        EditableObjectAction.EndEdit);

                    this._raiseEditingHandler(this._master, e);
                    if (e.Cancel)
                        return;
                }

                this._inEdit = false;

                if (this._raiseEditingHandler != null)
                {
                    this._raiseEditingHandler(this._master,
                        new EditingEventArgs(
                            this._master,
                            this.OriginalValues,
                            ItemChangeTiming.After,
                            EditableObjectAction.EndEdit));
                }

                this.OriginalValues.Clear();
            }
        }
    }
}


Top
 Profile  
 
 Post subject:
PostPosted: Fri Apr 25, 2008 3:59 am 
Newbie

Joined: Wed Feb 20, 2008 9:15 pm
Posts: 11
Sorry for the very late answer.
Your work looks very nice and I did it a quite similar way.

But I have another question. How do you handle Collections? NHibernate doesn't work with my custom built collections. Instead it uses its own type.

Any suggestions?


Top
 Profile  
 
 Post subject:
PostPosted: Fri Apr 25, 2008 9:18 am 
Expert
Expert

Joined: Fri Oct 28, 2005 5:38 pm
Posts: 390
Location: Cedarburg, WI
I've read there are ways to implement NHibernate collection interfaces in order to provide your own collection classes, but I don't think this was originally available (or it wasn't well documented/advertised) so we came up with the approach below to provide our own observable collection class. We did this back when we were using .NET 1.x; I don't know if it's now cleaner to implement an NHibernate collection interface.

Here's the pattern we use for collection properties. It requires field-based access in the mapping (we use camelcase-underscore). As far as NHibernate is concerned, it simply accesses the IList typed field. As far as our API is concerned, the collection property is nicely typed.

Code:
[Serializable]
public class SomeEntity : EntityBase
{
    // This field keeps the raw collection accessed by NHibernate
    private IList _someItems;

    // This field keeps our typed, observable, bindable,
    // item-ownership self-managing collection which can
    // contain and forward to an underlying ("external") collection
    [NonSerialized]
    private OurList<SomeItemType> _someItemsTyped;

    public virtual OurList<SomeItemType> SomeItems
    {
        bool hasPropertyChanged = false;
        object oldValue = this._someItemsTyped;

        if (this._someItemsTyped == null)
        {
            this._someItems = new OurList<SomeItemType>(
                this.SomeItemsUntyped,  // the underlying list
                ListOwnership.External, // tells our collection type to forward to another list
                this,                   // the list owner
                "SomeEntity");          // the "owner" property name on the item type
            hasPropertyChanged = true;
        }
        else if (this._someItemsTyped.ExternalList != this.SomeItemsUntyped)
        {
            this._someItemsTyped.ExternalList = this.SomeItemsUntyped;
            hasPropertyChanged = true;
        }

        if (hasPropertyChanged)
        {
            // This indicates that the collection reference itself has changed.
            // Our collection type exposes its own events for ItemAdded, ItemRemoved, etc.
            this.OnPropertyChanged("SomeItems", this._someItemsTyped);
        }

        return this._someItemsTyped;
    }

    private IList SomeItemsUntyped
    {
        get
        {
            if (this._someItems == null)
            {
                this._someItems = new ArrayList();
            }
            else
            {
                // This ensures that the collection is reattached
                // to the current session as needed, so that lazy loading
                // will work as expected.  This is provided not only for
                // convenience, but out of necessity since the untyped
                // collection that needs reattaching is not directly accessible
                // outside of this class!
                DetachedEntityHelper.ReattachAsNeeded(
                    this,
                    this._someItems,
                    typeof(SomeItemType));
            }

            return this._someItems;
        }
        set
        {
            this._someItems = value;
        }
    }
}


Top
 Profile  
 
 Post subject:
PostPosted: Sun Apr 27, 2008 3:52 pm 
Beginner
Beginner

Joined: Sun Oct 22, 2006 12:06 pm
Posts: 39
In NHibernate 2.0 there is great feature Filter.

I Had an issue with mystic object updates by NHibernate, by haveing my oun propertydescriptor I know exactly when update is required, so basicly now I could use class like
IEntityBase is my small Interface to detect if entity has changed

Code:
public class PreUpdateEventListener : IPreUpdateEventListener
    {
        #region IPreUpdateEventListener Members
        /// <summary>
        /// Function takes over update parameter chek, return true if update is not required, false if is required
        /// </summary>
        /// <param name="event"></param>
        /// <returns>Boolean as veto</returns>
        public bool OnPreUpdate(PreUpdateEvent @event)
        {
            IEntityBase entity = @event.Entity as IEntityBase;
            if (entity!=null)
                return !entity.IsChanged;
            else
                return false;
        }

        #endregion
    }


Top
 Profile  
 
 Post subject:
PostPosted: Mon Apr 28, 2008 8:33 pm 
Newbie

Joined: Wed Feb 20, 2008 9:15 pm
Posts: 11
pyhavai: I don't see how this would help me?! Please could you explain a litte more?

Nels_P_Olsen: looks interesting. So the "SomeEntity" must be mapped as a Nested Type in my class where i want to use that "collection"?


Top
 Profile  
 
 Post subject:
PostPosted: Thu May 01, 2008 1:07 pm 
Expert
Expert

Joined: Fri Oct 28, 2005 5:38 pm
Posts: 390
Location: Cedarburg, WI
In my example, SomeEntity is any entity of yours that will have collection properties. Your mapping would look something like this:
Code:
<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping
  xmlns="urn:nhibernate-mapping-2.2"
  default-access="field.camelcase-underscore"
  assembly="YourCompany.YourProduct.Entities"
  namespace="YourCompany.YourProduct.Entities"
>
  <class name="SomeEntity" table="some_parent_table" lazy="true" >
    <id column="some_parent_id" ...
    </id>
    ...
    <bag name="SomeItems" inverse="true" lazy="true" cascade="all" >
      <key column="some_parent_id" />
      <one-to-many class="SomeItemType" />
    </bag>
    ...
  </class>

  <class name="SomeItemType" table="some_child_table" lazy="true" >
    <id column="some_child_id" ...
    </id>
    ...
    <many-to-one name="SomeEntity" class="SomeEntity" cascade="none" column="some_parent_id" />
    ...
  </class>
</hibernate-mapping>


Top
 Profile  
 
 Post subject: ReflectionHelper Function missing.
PostPosted: Thu Sep 18, 2008 10:33 am 
Newbie

Joined: Mon Aug 11, 2008 4:54 am
Posts: 4
Hello Nels_P_Olsen,
i saw your very nice IEditableObject Implementation here. I am trying to include it in my NHibernation files. (Maybe in a Template for MyGeneration too). While testing it, i found that the Sub ReflectionHelper used in CancelEdit is missing in your code. Would you please post this Sub here?

Michael


Top
 Profile  
 
 Post subject:
PostPosted: Tue Sep 23, 2008 3:29 pm 
Expert
Expert

Joined: Fri Oct 28, 2005 5:38 pm
Posts: 390
Location: Cedarburg, WI
Our ReflectionHelper class (and related classes used by it) are monsters, so I'll just post the relevant methods here:

Code:
public static class ReflectionHelper
{
    ...

    public static void SetPropertyValue(object instance, string propertyName, object value)
    {
        ArgumentHelper.RequireValue("instance", instance);

        PropertyInfo property = GetProperty(instance.GetType(), propertyName, BindingFlags.Instance);

        if (property == null)
        {
            throw new MissingMemberException(instance.GetType().Name, propertyName);
        }

        try
        {
            SetPropertyValueCore(instance, property, value);
        }
        catch (Exception x)
        {
            throw new MemberAccessException(
                string.Format(
                    "Failed to set property {0}.{1} to '{2}' on instance '{3}'",
                    instance.GetType().Name,
                    propertyName,
                    value ?? "<null>",
                    instance.ToString()),
                x);
        }
    }

    public static PropertyInfo GetProperty(Type searchType, string propertyName, BindingFlags bindingFlags)
    {
        return GetProperty(searchType, propertyName, bindingFlags, false);
    }

    public static PropertyInfo GetProperty(Type searchType, string propertyName, BindingFlags bindingFlags, bool throwExceptionIfMissing)
    {
        ArgumentHelper.RequireValue("searchType",   searchType);
        ArgumentHelper.RequireValue("propertyName", propertyName);

        bindingFlags |= BindingFlags.Public;
        bindingFlags |= BindingFlags.NonPublic;
        bindingFlags |= BindingFlags.DeclaredOnly;

        while (searchType != null)
        {
            foreach (PropertyInfo property in searchType.GetProperties(bindingFlags))
            {
                if (property.Name == propertyName)
                {
                    return property;
                }
            }

            if (searchType.IsInterface)
            {
                PropertyInfo inheritedInterfaceProperty;
                foreach (Type subInterfaceType in searchType.GetInterfaces())
                {
                    inheritedInterfaceProperty = GetProperty(
                        subInterfaceType, propertyName, bindingFlags);

                    if (inheritedInterfaceProperty != null)
                    {
                        return inheritedInterfaceProperty;
                    }
                }
            }
            else
            {
                searchType = searchType.BaseType;
            }
        }

        if (throwExceptionIfMissing)
        {
            throw new MissingMemberException(searchType.FullName, propertyName);
        }

        return null;
    }

    private static void SetPropertyValueCore(
        object instance,
        PropertyInfo property,
        object value)
    {
        try
        {
            property.SetValue(instance, value, null);
        }
        catch (InvalidCastException)
        {
            object convertedValue = Convert.ChangeType(value, property.PropertyType);
            property.SetValue(instance, convertedValue, null);
        }
    }
}

/// <summary>
/// A helper class for enforcing requirements on method and property arguments.
/// </summary>
/// <remarks>
/// Using this class allows you to write concise, declarative-style code
/// for enforcing argument requirements.  The methods provided cover the large
/// majority of such needs, freeing your code of "if/then" clutter while providing
/// consistent exception types and messages.
/// </remarks>
public static class ArgumentHelper
{
    /// <summary>
    /// Requires an argument to implement a given type
    /// (throwing an exception if it does not).
    /// </summary>
    /// <param name="requiredType">The type that the argument must implement.</param>
    /// <param name="argumentName">The actual name of the argument or property.</param>
    /// <param name="argumentValue">The value to check.</param>
    /// <exception cref="ArgumentOutOfRangeException">
    /// <paramRef>argumentValue</paramRef> does not implement
    /// <paramRef>requiredType</paramRef>.
    /// </exception>
    public static void RequireType(
        Type requiredType, string argumentName, object argumentValue)
    {
        RequireValue("requiredType", requiredType);
        RequireValue("argumentName", argumentName);

        if (argumentValue != null)
        {
            RequireType(requiredType, argumentName, argumentValue.GetType());
        }
    }

    /// <summary>
    /// Requires an argument to implement a given type
    /// (throwing an exception if it does not).
    /// </summary>
    /// <param name="requiredType">The type that the argument must implement.</param>
    /// <param name="argumentName">The actual name of the argument or property.</param>
    /// <param name="argumentType">The type to check.</param>
    /// <exception cref="ArgumentOutOfRangeException">
    /// <paramRef>argumentType</paramRef> does not implement
    /// <paramRef>requiredType</paramRef>.
    /// </exception>
    public static void RequireType(
        Type requiredType, string argumentName, Type argumentType)
    {
        RequireValue("requiredType", requiredType);
        RequireValue("argumentName", argumentName);
        RequireValue("argumentType", argumentType);

        if (!requiredType.IsAssignableFrom(argumentType))
        {
            string argumentTypeName;
            string requiredTypeName;

            if (argumentType.FullName != requiredType.FullName)
            {
                argumentTypeName = argumentType.FullName;
                requiredTypeName = requiredType.FullName;
            }
            else if (argumentType.AssemblyQualifiedName != requiredType.AssemblyQualifiedName)
            {
                argumentTypeName = argumentType.AssemblyQualifiedName;
                requiredTypeName = requiredType.AssemblyQualifiedName;
            }
            else
            {
                argumentTypeName = string.Format(
                    CultureInfo.CurrentCulture,
                    "{0} loaded from {1}",
                    argumentType.AssemblyQualifiedName,
                    SubstituteWhereEmpty(argumentType.Assembly.Location, "(unknown)"));

                requiredTypeName = string.Format(
                    CultureInfo.CurrentCulture,
                    "{0} loaded from {1}",
                    requiredType.AssemblyQualifiedName,
                    SubstituteWhereEmpty(requiredType.Assembly.Location, "(unknown)"));
            }

            throw new ArgumentOutOfRangeException(argumentName, argumentType,
                string.Format(
                    "Argument '{0}' type '{1}' is not assignable to type '{2}'.",
                    argumentName,
                    argumentTypeName,
                    requiredTypeName));
        }
    }

    /// <summary>
    /// Requires an argument to have a value
    /// (throwing an exception if it does not).
    /// </summary>
    /// <param name="argumentName">The actual name of the argument or property.</param>
    /// <param name="argumentValue">The collection to check.</param>
    /// <exception cref="ArgumentNullException">
    /// <paramRef>argumentName</paramRef> or <paramRef>argumentValue</paramRef>
    /// is <c>null</c>.
    /// </exception>
    /// <exception cref="ArgumentNotValuedException">
    /// <paramRef>argumentName</paramRef> or <paramRef>argumentValue</paramRef>
    /// has a trimmed length of zero.
    /// </exception>
    public static void RequireValue(string argumentName, object argumentValue)
    {
        if (argumentValue == null)
        {
            throw new ArgumentNullException(argumentName);
        }
        else
        {
            // TODO: make an IRequireValue interface for classes and services.
            // Then provide a way to register these interfaces by type,
            // and automatically register the two below.
            //
            // That way, this front-end service can always be used,
            // without know any details of how to determine if a "value" is present
            // (some types can be "unvalued" even when not null.)
            // It will also allow for when a separate service must be used,
            // such as the case for strings, because we can't subclass string
            // to put an HasValue() method on it ...

            switch (argumentValue.GetType().FullName)
            {
                case "System.String":
                    TextHelper.RequireValue(argumentName, argumentValue);
                    break;

                case "System.Text.StringBuilder":
                    TextHelper.RequireValue(argumentName, argumentValue.ToString());
                    break;
            }
        }
    }

    /// <summary>
    /// Requires a collection have at least one element
    /// (throwing an exception if it does not).
    /// </summary>
    /// <param name="argumentName">The actual name of the argument or property.</param>
    /// <param name="argumentValue">The collection to check.</param>
    /// <exception cref="ArgumentNullException">
    /// <paramRef>argumentName</paramRef> or <paramRef>argumentValue</paramRef>
    /// is <c>null</c>.
    /// </exception>
    /// <exception cref="ArgumentNotValuedException">
    /// <paramRef>argumentValue</paramRef> has no elements,
    /// or <paramRef>argumentName</paramRef> or <paramRef>argumentValue</paramRef>
    /// has a trimmed length of zero.
    /// </exception>
    public static void RequireNonEmptyCollection(
        string argumentName, ICollection argumentValue)
    {
        RequireValue(argumentName, argumentValue);

        if (argumentValue.Count == 0)
        {
            throw new ArgumentNotValuedException(argumentName, argumentValue,
                "The collection must have at least one element.");
        }
    }

    /// <summary>
    /// Return either the specified value, or its substitute if empty.
    /// </summary>
    /// <param name="valueToTry">The value to return, if not empty.</param>
    /// <param name="valueToSubstitute">The value to return if
    /// <paramref>valueToTry</paramref> is empty.</param>
    /// <returns><paramref>valueToTry</paramref> if not empty,
    /// otherwise <paramref>valueToSubstitute</paramref>.</returns>
    /// <remarks>
    /// Values of <b>Null</b> <see cref="DBNull.Value"/> are considered empty.
    /// Blank strings or strings containing only whitespace are also considered empty.
    /// </remarks>
    public static object SubstituteWhereEmpty(object valueToTry, object valueToSubstitute)
    {
        return (TextHelper.HasValue(valueToTry) ? valueToTry : valueToSubstitute);
    }
}

/// <summary>
/// Indicates that an argument, while not null, is still "unvalued"
/// in some way specific to the argument type, that makes it invalid
/// for use by the receiving method or property.
/// </summary>
/// <example>
/// <see cref="ArgumentHelper.RequireValue">
/// ArgumentHelper.RequireValue</see> requires arguments of type String and
/// StringBuilder to contain values with non-zero trimmed lengths.
/// <see cref="ArgumentHelper.RequireNonEmptyCollection">
/// ArgumentHelper.RequireNonEmptyCollection</see> requires specified
/// collections to have at least one element.
/// Both of these methods throw this type of exception
/// if their requirements are not met.
/// </example>
///
[Serializable]
public class ArgumentNotValuedException : ArgumentOutOfRangeException
{
    public ArgumentNotValuedException(
        string argumentName, object value, string message)
        : base(argumentName, value, message)
    {
    }

    public ocArgumentNotValuedException()
        : base()
    {
    }

    public ArgumentNotValuedException(string message, Exception innerException)
        : base(message, innerException)
    {
    }

    public ArgumentNotValuedException(string argumentName)
        : base(argumentName)
    {
    }

    protected ArgumentNotValuedException(
        SerializationInfo info,
        StreamingContext context)
        : base(info, context)
    {
    }
}

public static class TextHelper
{
    ...

    public static void RequireValue(string argumentName, object argumentValue)
    {
        if (argumentValue == null)
        {
            throw new ArgumentNullException(argumentName);
        }

        if (!HasValue(argumentValue))
        {
            throw new ArgumentNotValuedException(argumentName, argumentValue,
                "The argument must have a non-zero trimmed length.");
        }
    }

    /// <summary>
    /// Determines if a value is usable (non-<b>null</b> and non-blank).
    /// </summary>
    /// <param name="value">The value to check.</param>
    /// <returns>
    /// <b>true</b> if <paramref>value</paramref> is non-<b>null</b> and non-blank,
    /// otherwise <b>false</b>.
    /// </returns>
    public static bool HasValue(object value)
    {
        return (NullToBlankString(value).Trim().Length > 0);
    }

    public static string NullToBlankString(object parameter)
    {
        object cleanedParameter = DbNullToNull(parameter);
        return (cleanedParameter == null ? string.Empty : cleanedParameter.ToString());
    }

    public static object DbNullToNull(object parameter)
    {
        object result;

        if (parameter != null && parameter.GetType() == System.DBNull.Value.GetType())
        {
            result = null;
        }
        else
        {
            result = parameter;
        }

        return result;
    }
}


Top
 Profile  
 
 Post subject: ReflectionHelper Function missing.
PostPosted: Mon Sep 29, 2008 4:25 am 
Newbie

Joined: Mon Aug 11, 2008 4:54 am
Posts: 4
Hello Nels,
sorry for my later answare, but I am a little bit 'under pressure' these days ;-). Thanks for you Post, it helps me a lot! Now I can implement a lot more in my Business logic.

Again: many thanks!

Michael


Top
 Profile  
 
 Post subject: Re: How to detect whether object has changes or not
PostPosted: Thu Nov 17, 2011 11:48 pm 
Newbie

Joined: Thu Nov 17, 2011 11:41 pm
Posts: 1
Hi Nels,

I've read your post and found it extremely useful and interesting. I would like to implement you method for our business objects, but I did face some issues with the collections. Can you please guide me through this? It seems it is missing a collection helper class ( DetachedEntityHelper ). Maybe the code for "OurList" class also?

Thank you,
Alin


Top
 Profile  
 
Display posts from previous:  Sort by  
Forum locked This topic is locked, you cannot edit posts or make further replies.  [ 14 posts ] 

All times are UTC - 5 hours [ DST ]


You cannot post new topics in this forum
You cannot reply to topics in this forum
You cannot edit your posts in this forum
You cannot delete your posts in this forum

Search for:
© Copyright 2014, Red Hat Inc. All rights reserved. JBoss and Hibernate are registered trademarks and servicemarks of Red Hat, Inc.