DataObjectBase and IDataErrorInfo

Oct 25, 2011 at 3:23 PM

I feel like I must be missing something, but it appears that DataObjectBase does not properly implement IDataErrorInfo.  I don't see how Catel.Core would even compile if this were the case, though.

DataObjectBase implements IDataObjectBase, which implements, among other interfaces, IDataErrorInfo.  IDataErrorInfo contains a property: string Error.  However, I can't find an Error property in DataObjectBase, or ObservableObject, which DataObjectBase derives from.  How can this be?  How does Catel.Core build without errors if an interface is not fully implemented?

I discovered this because I am getting a PropertyNotFoundException during validation.  My ValidateBusinessRules override is setting a business error, which causes DataObjectBase.NotifyErrorsChanged to raise a PropertyChanged event with IDataErrorInfo.Error as the property that is changed.  The PropertyHelper.GetPropertyValue method then throws an exception, saying that "Property 'IDataErrorInfo.Error' is not found."

 

I am using the latest beta release of 2.3 (from Friday, 10/21) and the Net4.0 version of Catel assemblies.

Oct 25, 2011 at 4:13 PM

Catel implements the IDataErrorInfo explicitly. Now the question: how on earth to do validation? We have some documenation about that:

http://catel.catenalogic.com/index.html?using_the_classes.htm

So, actually, the only thing you have to do is either override ValidateFields or ValidateBusinessRules.

The question is: why is it triggering the PropertyHelper, because the IDataErrorInfo should be ignored. Can you show some code here? Thanks in advance!

Oct 25, 2011 at 7:23 PM
Edited Oct 25, 2011 at 7:28 PM

Thanks, Geert.  I knew I was missing something simple - I now see the explicit implementation.  Part of what was throwing me off is that if you try to find it in the "DataObjectBase [from metadata] file that VS creates when you access a definition in an external module, it doesn't show up there.  I never realized that interface implementations don't show up in the metadata files.

Regarding the code, this is a bit lengthy to explain, but I think this may expose a bug in Catel 2.3 - especially since you said that IDataErrorInfo should be ignored.

I have an object called ControlNameAndType that derives from DataBaseObject.  This object has 3 Catel Properties:

- ControlRow (a DataRow from a typed dataset)

- CtrlType (string)

- Name (string). 

Here is the first part of the class definition, including the constructor.

    public class ControlNameAndType : DataObjectBase<ControlNameAndType>
    {
        #region Variables
        #endregion

        #region Constructor & destructor
        /// <summary>
        /// Initializes a new object from scratch.
        /// </summary>
        public ControlNameAndType() { }

        public ControlNameAndType(CosmosDS.ControlRow controlRow, string name, string ctrlType) : base()
        {
            Name = name; CtrlType = ctrlType; ControlRow = controlRow;
        }

I have overridden ValidateBusinessRules like so:
        protected override void ValidateBusinessRules(List<BusinessRuleValidationResult> validationResults)
        {
            //make sure all three values are set, if any are set
            if (ControlRow != null || !string.IsNullOrEmpty(CtrlType) || !string.IsNullOrEmpty(Name))
            {
                if (ControlRow == null)
                {
                    validationResults.Add(new BusinessRuleValidationResult(ValidationResultType.Error, "ControlRow cannot be null"));
                }
            }
        }

Here is the sequence of steps that lead to the exception. I am leaving out several methods in the call stack and focusing in on the key ones:
1- Create a new ControlNameAndType object in my ViewModel using the ctor that accepts parameters for ControlRow, Name, and Type.
2- The ControlNameAndType ctor sets the Name property to the name argument passed in.
3- Since DataBaseObject sets AutomaticallyValidateOnPropertyChanged = true in its constructor, DataObjectBase.Validate() gets called in this DataBaseObject method:
        private void RaisePropertyChanged(object sender, PropertyChangedEventArgs e, bool setDirtyAndAllowAutomaticValidation)

4- At this point, the ControlRow property is still null, so my ValidateBusinessRules method adds a new Business Error.
5- DataObjectBase.NotifyErrors gets called, which in turn calls RaisePropertyChanged(ErrorMessageProperty). Note that ErrorMessageProperty is a const string set to "IDataErrorInfo.Error".
6- In the ctor for AdvancedPropertyChangedEventArgs, the following code executes:
            // Last resort to get the new value
            if (!isNewValueMeaningful)
            {
                if (PropertyHelper.TryGetPropertyValue(originalSender, propertyName, out newValue))
                {
                    isNewValueMeaningful = true;
                }
            }

7- TryGetPropertyValue calls GetPropertyValue, wherein the following code causes the exception
 
Type type = obj.GetType();
PropertyInfo propertyInfo = type.GetProperty(property, PropertyBindingFlags);
if (propertyInfo == null)
{
    Log.Error("Property '{0}' is not found on the object '{1}', probably the wrong field is being mapped", property, type);
    throw new PropertyNotFoundException(property);
}

IDataErrorInfo.Error is a protected property, which is why PropertyHelper.GetPropertyValue() can't find it.

Here is the complete stack trace (starting from the ControlNameAndType ctor):

>    Catel.Core.dll!Catel.Reflection.PropertyHelper.GetPropertyValue(object obj, string property) Line 141    C#
     Catel.Core.dll!Catel.Reflection.PropertyHelper.TryGetPropertyValue(object obj, string property, out object value) Line 75 + 0x11 bytes    C#
     Catel.Core.dll!Catel.Data.AdvancedPropertyChangedEventArgs.AdvancedPropertyChangedEventArgs(object originalSender, object latestSender, string propertyName, object oldValue, object newValue, bool isOldValueMeaningful, bool isNewValueMeaningful) Line 111 + 0xf bytes    C#
     Catel.Core.dll!Catel.Data.AdvancedPropertyChangedEventArgs.AdvancedPropertyChangedEventArgs(object sender, string propertyName) Line 39 + 0x19 bytes    C#
     Catel.Core.dll!Catel.Data.ObservableObject.RaisePropertyChanged(object sender, string propertyName) Line 244 + 0x1b bytes    C#
     Catel.Core.dll!Catel.Data.ObservableObject.RaisePropertyChanged(string propertyName) Line 212 + 0xe bytes    C#
     Catel.Core.dll!Catel.Data.DataObjectBase.NotifyErrorsChanged(string propertyName, bool notifyHasErrors) Line 2717 + 0xe bytes    C#
     Catel.Core.dll!Catel.Data.DataObjectBase.Validate(bool force, bool notifyChangedPropertiesOnly) Line 2685 + 0x10 bytes    C#
     Catel.Core.dll!Catel.Data.DataObjectBase.Validate(bool force) Line 2517 + 0xe bytes    C#
     Catel.Core.dll!Catel.Data.DataObjectBase.Validate() Line 2505 + 0xa bytes    C#
     Catel.Core.dll!Catel.Data.DataObjectBase.RaisePropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e, bool setDirtyAndAllowAutomaticValidation) Line 2245 + 0x8 bytes    C#
     Catel.Core.dll!Catel.Data.DataObjectBase.RaisePropertyChanged(object sender, Catel.Data.AdvancedPropertyChangedEventArgs e) Line 2155 + 0x10 bytes    C#
     Catel.Core.dll!Catel.Data.ObservableObject.RaisePropertyChanged(object sender, string propertyName, object oldValue, object newValue) Line 275 + 0x11 bytes    C#
     Catel.Core.dll!Catel.Data.ObservableObject.RaisePropertyChanged(string propertyName, object oldValue, object newValue) Line 233 + 0x14 bytes    C#
     Catel.Core.dll!Catel.Data.DataObjectBase.SetValue(string name, object value, bool notifyOnChange) Line 1182 + 0x11 bytes    C#
     Catel.Core.dll!Catel.Data.DataObjectBase.SetValue(string name, object value) Line 1100 + 0x10 bytes    C#
     Catel.Core.dll!Catel.Data.DataObjectBase.SetValue(Catel.Data.PropertyData property, object value) Line 1198 + 0x21 bytes    C#
     CosmosEditor.exe!CosmosEditor.ModelLayer.Data.ControlNameAndType.Name.set(string value) Line 51 + 0x12 bytes    C#
     CosmosEditor.exe!CosmosEditor.ModelLayer.Data.ControlNameAndType.ControlNameAndType(CosmosEditor.ModelLayer.Data.CosmosDS.ControlRow controlRow, string name, string ctrlType) Line 30 + 0xb bytes    C#



 


 
 
Oct 26, 2011 at 3:29 PM

I will investigate this tonight and let you know! Thanks for the detailed info.

Oct 26, 2011 at 3:37 PM

Just a quick question: since TryGetValue is used, it should be safe to call the method. Do you get the exception because you have enabled the notification of exception, when then they are correctly handled? Or is it actually thrown to you?

Oct 27, 2011 at 2:45 AM

Yes - you are correct - I have VS set to stop on all exceptions.  The exception gets caught in TryGetValue.  so, this isn't a catastrophic problem.