IDataErrorInfo Behavior Using DynamicProxy

Castle Windsor sample application now supports IDataErrorInfo interface. If you have read my post on how to implemnent INotifyPropertyChanged interface using Castle DynamicProxy, here same technique is used in parallel with Fluent Validation framework for Silverlight. The magic behind this approach is that your models are clear as you want them to be. Let's see in more detail how this can be done.

A typical implementation of IDataErrorInfo interface is like the following, where you need to specify two things when object state has errors: A. Using the indexer you specify if a property value on the object has any error B. Error property that returns a general error message when object has any errors.

Let's see how this looks on our simple Customer object:

namespace Castle.Samples.WindsorSilverlight.Model
{
    using System;

    public class Customer : IDataErrorInfo
    {
        public virtual int Age { get; set; }

        public virtual DateTime JoinedAt { get; private set; }

        public virtual string Firstname { get; set; }

        public virtual string Lastname { get; set; }

        public virtual string this[string columnName]
        {
            get
            {
                switch (columnName)
                {
                    case "Age":
                        return Age < 18 || Age > 60 ? "Age is not valid" : string.Empty;
                    case "Firstname":
                        return string.IsNullOrEmpty(Firstname) ? "Firstname is mandatory" : string.Empty;
                    case "Lastname":
                        return string.IsNullOrEmpty(Lastname) ? "Lastname is mandatory" : string.Empty;
                    default:
                        return string.Empty;
                }
            }
        }

        private bool HasAnyError()
        {
            return Age < 18 || Age > 60 ||
                   string.IsNullOrEmpty(Firstname) ||
                   string.IsNullOrEmpty(Lastname);
        }

        public virtual string Error
        {
            get { return HasAnyError() ? "Customer is not valid" : string.Empty; }
        }
    }
}

As you can see, our once clean object, is cluttered. One reason is that the validation logic is inside the same class (SoC anyone?), but even if we remove that, we'd have the interface implementation that delegate the dirty work to a validator, but again the noise is still there.

The solution is to use DataErrorInfoBehavior. Using this approach you don't even need to implement the interface on your model class and it will be done for you dynamically. All you need to do is to register and resolve your objects from the container, and let it do all the heavy lifting for you.

To make the models resolved from the container implement IDataErrorInfo interface for you, first you need to specify the additional interface that you require, and then add an interceptor to implement that interface dynamically. The following snippet will register your Customer class along with necessary interface and interceptor:

public class ModelInstaller : IWindsorInstaller
{
    public void Install(IWindsorContainer container, IConfigurationStore store)
    {
        //Add interceptors and behaviors to the object
        container.Register(Component.For<Customer>()
                 .LifeStyle.Transient
                 .Proxy.AdditionalInterfaces(typeof(IDataErrorInfo))
                 .Interceptors(typeof(DataErrorInfoBehavior)));
    }
}

All the magic is in the DataErrorInfoBehavior class. Here we'll validate the object based on what method of the IDataErrorInfo is being called and set the return value of that method:

public void Intercept(IInvocation invocation)
{
    if (invocation.Method.DeclaringType.Equals(typeof(IDataErrorInfo)))
    {
        var validator = GetValidator(typeof (Customer)); 
        var result = validator.Validate(invocation.Proxy);

        if(result.IsValid) //object is valid
        {
            invocation.ReturnValue = string.Empty;
            return;
        }

        if (invocation.Method.Name == "get_Item") //Indexer is called, get the error for the property
        {
            var propertyName = (string) invocation.Arguments[0]; //property name to validate
            var errors = result.Errors.Where(x => x.PropertyName.Contains(propertyName));

            invocation.ReturnValue = string.Join(Environment.NewLine, errors);
        }
        else if (invocation.Method.Name == "get_Error") //Error property is called
        {
            invocation.ReturnValue = "Customer is not valid"
        }
    }
    else
    {
        //Other methods not belonging to IDataErrorInfo,
        //so just invoke the original method.
        invocation.Proceed();
    }
}

If you resolve an instance of your object from the container and bind it to a view, you'll see that this is working pretty sweet.

If you are interested, you can check out the complete implementation along with other goodies in Castle Windsor Example Application over at GitHub.

No Comments

  • dedalus said

    I see your sample project on https://github.com/HEskandari/Castle-Windsor-Examples : it's fantastic!

    Do you have ported also to WPF side? I try to create same example on WPF 4 + castle 2.5 in my spare time (with brutal approach, I have never used Silverlight) but don't work the same

  • Hadi Eskandari said

    @dedalus: No I haven't ported the sample to WPF, but it should be straightforward. What was the problem you encountered?

  • dedalus said

    I have ported 95% of the code (5% is commented) and I have this:

    System.IO.IOException was unhandled
    Message=Cannot locate resource 'mainview.xaml'.
    Source=PresentationFramework
    StackTrace:
    in MS.Internal.AppModel.ResourcePart.GetStreamCore(FileMode mode, FileAccess access)
    in System.IO.Packaging.PackagePart.GetStream(FileMode mode, FileAccess access)
    in System.IO.Packaging.PackagePart.GetStream()
    in System.Windows.Application.LoadComponent(Uri resourceLocator, Boolean bSkipJournaledProperties)
    in System.Windows.Application.DoStartup()
    in System.Windows.Application.<.ctor>b__1(Object unused)
    in System.Windows.Threading.ExceptionWrapper.InternalRealCall(Delegate callback, Object args, Int32 numArgs)
    in MS.Internal.Threading.ExceptionFilterHelper.TryCatchWhen(Object source, Delegate method, Object args, Int32 numArgs, Delegate catchHandler)
    in System.Windows.Threading.DispatcherOperation.InvokeImpl()
    in System.Windows.Threading.DispatcherOperation.InvokeInSecurityContext(Object state)
    in System.Threading.ExecutionContext.runTryCode(Object userData)
    in System.Runtime.CompilerServices.RuntimeHelpers.ExecuteCodeWithGuaranteedCleanup(TryCode code, CleanupCode backoutCode, Object userData)
    in System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state)
    in System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean ignoreSyncCtx)
    in System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
    in System.Windows.Threading.DispatcherOperation.Invoke()
    in System.Windows.Threading.Dispatcher.ProcessQueue()
    in System.Windows.Threading.Dispatcher.WndProcHook(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam, Boolean& handled)
    in MS.Win32.HwndWrapper.WndProc(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam, Boolean& handled)
    in MS.Win32.HwndSubclass.DispatcherCallbackOperation(Object o)
    in System.Windows.Threading.ExceptionWrapper.InternalRealCall(Delegate callback, Object args, Int32 numArgs)
    in MS.Internal.Threading.ExceptionFilterHelper.TryCatchWhen(Object source, Delegate method, Object args, Int32 numArgs, Delegate catchHandler)
    in System.Windows.Threading.Dispatcher.InvokeImpl(DispatcherPriority priority, TimeSpan timeout, Delegate method, Object args, Int32 numArgs)
    in MS.Win32.HwndSubclass.SubclassWndProc(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam)
    in MS.Win32.UnsafeNativeMethods.DispatchMessage(MSG& msg)
    in System.Windows.Threading.Dispatcher.PushFrameImpl(DispatcherFrame frame)
    in System.Windows.Threading.Dispatcher.PushFrame(DispatcherFrame frame)
    in System.Windows.Threading.Dispatcher.Run()
    in System.Windows.Application.RunDispatcher(Object ignore)
    in System.Windows.Application.RunInternal(Window window)
    in System.Windows.Application.Run(Window window)
    in System.Windows.Application.Run()
    in HEskandari.WPF.CastleSample.App.Main() in F:\HEskandari\HEskandari.WPF.CastleSample\obj\x86\Debug\App.g.cs:riga 0
    in System.AppDomain._nExecuteAssembly(RuntimeAssembly assembly, String[] args)
    in System.AppDomain.ExecuteAssembly(String assemblyFile, Evidence assemblySecurity, String[] args)
    in Microsoft.VisualStudio.HostingProcess.HostProc.RunUsersAssembly()
    in System.Threading.ThreadHelper.ThreadStart_Context(Object state)
    in System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean ignoreSyncCtx)
    in System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
    in System.Threading.ThreadHelper.ThreadStart()
    InnerException:


    I have created a newest WPF windows, but I have simple copied the xaml part that define the controls on Silverlight windows... it's that I'm using some Silverlight only object?

    Thanks for your reply

  • dedalus said

    I have maked some step forward: now I have translate 99,5% of the code in WPF, but I have a generic error on startup like this:

    System.Windows.Markup.XamlParseException was unhandled
    No constructor for type 'HEskandari.CastleFramework.WPF.Views.MainView'. Try to use Arguments or FactoryMethod directive to build the type (translate for italian).

    I have experimented that if I add a empty constructor to MainView, the application start, but it seems that don't bind commands.

    Any idea?

    ps: about comment #4, I have replaced RootVisual with MainWindow

Add a Comment