Click here to Skip to main content
15,886,518 members
Articles / Web Development

WPF Two-way Databinding in ASP.NET - Enabling MVVM

Rate me:
Please Sign up or sign in to vote.
4.88/5 (35 votes)
29 Jan 2011CPOL21 min read 139.4K   66  
Bringing WPF like declarative data binding to ASP.NET Web Forms to enable declarative two-way data binding to any object whilst opening up MVVM UI development.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.ComponentModel;
using System.Collections;
using System.Web.UI;
using System.Text;
using System.Web.UI.WebControls;
using System.Windows.Input;
using System.Reflection;
using System.Text.RegularExpressions;
using System.Diagnostics;
using System.Windows.Data;
using System.Globalization;
using Binding.Services;
using Binding.ControlWrappers;
using Binding.Helpers;
using Binding.Interfaces;

namespace Binding
{
    /// <summary>
    /// Provides Binding Services to an IBindingContainer
    /// </summary>
    public abstract class BinderBase
    {

        #region Internal Data

        private const string DATA_BINDING_KEY = "DATA_BINDINGS";
        private const string COMMAND_BINDING_KEY = "COMMAND_BINDINGS";
        private const string BINDING_SEQUENCE_MANAGER = "BINDING_SEQUENCE_MANAGER";

        private bool performBind = false;
        private bool isPushing = false;

        private object dataContext;
        public virtual object DataContext
        {
            get { return dataContext; }
            protected set { dataContext = value; }
        }

        private UpdateSourceTrigger updateSourceTrigger;
        public UpdateSourceTrigger UpdateSourceTrigger
        {
            get { return updateSourceTrigger; }
            set { updateSourceTrigger = value; }
        }
        
        private IBindingContainer bindingContainer;
        public IBindingContainer BindingContainer
        {
            get { return bindingContainer; }
        }


        private IDataStorageService dataStorageService;
        protected IDataStorageService DataStorageService
        {
            get { return dataStorageService; }
        }

        private IControlService controlService;
        protected IControlService ControlService
        {
            get { return controlService; }
        }

        private BindingCollection CommandStorage
        {
            get
            {
                return DataStorageService.RetrieveOrCreate<BindingCollection>(COMMAND_BINDING_KEY);
            }
        }

        private BindingCollection DataStorage
        {
            get
            {
                return DataStorageService.RetrieveOrCreate<BindingCollection>(DATA_BINDING_KEY);
            }
        }

        #endregion 

        public BinderBase(IBindingContainer bindingContainer, IDataStorageService dataStorageService, IControlService controlService)
        {
            this.bindingContainer = bindingContainer;
            this.dataStorageService = dataStorageService;
            this.controlService = controlService;

            bindingContainer.Load += new EventHandler(bindingContainer_Load);
        }

        #region Bind

        /// <summary>
        /// Execute a bind and return the result
        /// </summary>
        /// <remarks>
        /// This method will attempt to auto resolve the property of the target control to which to 
        /// bind. The implementation of this resolution can be control via IControlService.TryGetTargetPropertyName.
        /// </remarks>
        /// <param name="control">The target control</param>
        /// <param name="bindingOptions"></param>
        /// <returns>The result (value) of the bind</returns>
        public object ExecuteBind(IBindingTarget control, Options bindingOptions)
        {

            string propertyName = string.Empty;
            object value = null;
            
            if (controlService.TryGetTargetPropertyName(control, bindingOptions, out propertyName))
                value= ExecuteBind(control, bindingOptions, propertyName);

            return value;
        }

        /// <summary>
        /// Execute a bind and return the result
        /// </summary>
        /// <param name="controlID">The target control</param>
        /// <param name="bindingOptions"></param>
        /// <param name="targetPropertyName">The property on the target control which you wish to bind</param>
        /// <returns></returns>
        public object ExecuteBind(IBindingTarget control, Options bindingOptions, string targetPropertyName)
        {
            return ExecuteBind(control, null, bindingOptions, targetPropertyName);
        }

        /// <summary>
        /// Execute a bind in the context of a collection container. 
        /// </summary>
        /// <remarks>
        /// This method will attempt to auto resolve the property of the target control to which to 
        /// bind. The implementation of this resolution can be control via IControlService.TryGetTargetPropertyName.
        /// </remarks>
        /// <param name="control">The target control</param>
        /// <param name="collectionItemContainer">
        /// The collection item container. E.g. In the case of a repeater a RepeaterItem.
        /// This is available from the ItemTemplate control via (IDataItemContainer)container.NamingContainer. 
        /// </param>
        /// <param name="bindingOptions"></param>
        /// <returns>The result (value) of the bind</returns>
        public object ExecuteBind(IBindingTarget control, IBindingTarget<IDataItemContainer> collectionItemContainer, Options bindingOptions)
        {
            string propertyName = string.Empty;
            object value = null;
            
            if (controlService.TryGetTargetPropertyName(control, bindingOptions, out propertyName))
              value = ExecuteBind(control, collectionItemContainer, bindingOptions, propertyName);
            
            return value;
        }
        
        /// <summary>
        /// Execute a bind in the context of a collection container. 
        /// </summary>
        /// <remarks>
        /// Specficy the target control directly.
        /// </remarks>
        /// <param name="control">The target control</param>
        /// <param name="collectionItemContainer">
        /// The collection item container. E.g. In the case of a repeater a RepeaterItem.
        /// This is available from the ItemTemplate control via (IDataItemContainer)container.NamingContainer. 
        /// </param>
        ///<param name="targetPropertyName">The property on the targetcontrol.</param> 
        /// <param name="bindingOptions"></param>
        /// <returns>The result (value) of the bind</returns>
        public object ExecuteBind(IBindingTarget control, IBindingTarget<IDataItemContainer> collectionItemContainer, Options bindingOptions, string targetPropertyName)
        {
            if (control == null || bindingContainer == null ||  bindingOptions == null || string.IsNullOrEmpty(bindingOptions.Path))
                return null;

            object dataValue = null;
            BindingDef binding = null;
            
            IBindingTarget genericContainer = null;
            object baseContext = this.DataContext;

            if (bindingOptions.PathMode == PathMode.Relative && collectionItemContainer != null)
            {
                IDataItemContainer containerControl = collectionItemContainer.TargetInstance;
                dataValue = DataBinder.Eval(containerControl.DataItem, bindingOptions.Path);
                bool isCollectionBinding = DetermineIfCollection(control, dataValue);
                binding = new BindingDef(control, targetPropertyName, containerControl.DataItemIndex, DataStorage, bindingOptions, isCollectionBinding, controlService);
                genericContainer = collectionItemContainer;
            }
            else
            {
                dataValue = DataBinder.Eval(baseContext, bindingOptions.Path);
                bool isCollectionBinding = DetermineIfCollection(control, dataValue);
                binding = new BindingDef(control, targetPropertyName, bindingOptions, isCollectionBinding, controlService);
                genericContainer = controlService.GetParent(control);
            }

            //determine binding direction is not explicitly set based on the value and contorl
            if (bindingOptions.Mode == BindingMode.Unset)
                bindingOptions.Mode = DetermineBindingDirection(control, dataValue);

            //store the binding
            DataStorage.Add(binding);

            return AttemptConvert(binding, dataValue, genericContainer);
        }

        /// <summary>
        /// Bind to a command with auto resolution of the TargetProperty
        /// </summary>
        /// <param name="sourceExpression"></param>
        /// <param name="targetExpression"></param>
        public void ExecuteCommandBind(IBindingTarget control, Options bindingOptions)
        {
            string propertyName = string.Empty;

            if (controlService.TryGetTargetPropertyName(control, bindingOptions, out propertyName))
                ExecuteCommandBind(control, bindingOptions, propertyName);
           
        }

        /// <summary>
        /// Bind to a command when the TargetProperty is known
        /// </summary>
        /// <param name="control"></param>
        /// <param name="bindingOptions"></param>
        /// <param name="targetPropertyName"></param>
        public void ExecuteCommandBind(IBindingTarget control, Options bindingOptions, string targetPropertyName)
        {
            BindingDef binding = new BindingDef(control, targetPropertyName, bindingOptions, controlService);
            CommandStorage.Add(binding);
            ExecuteCommandBind(control, binding);
        }

        private void ExecuteCommandBind(BindingDef binding)
        {
            TargetProperty target = binding.ResolveAsTarget(this.BindingContainer);
            ExecuteCommandBind(target.OwningControl, binding);
        }
        
        private void ExecuteCommandBind(IBindingTarget control, BindingDef binding)
        {

            //Get the ICommandObject from the source
            SourceProperty commandInstance = binding.SourceExpression.ResolveAsSource(this.DataContext);
            object commandObject = commandInstance.Value;
            ICommand iCommand = commandObject as ICommand;

            //Get the property setter for the event to which we will bind            
            TargetEvent evt = binding.ResolveAsTargetEvent(this.bindingContainer);

            //bind
            Type eventDelegateType = evt.Descriptor.EventHandlerType;
            Delegate handler = new EventHandler((s, e) => iCommand.Execute(this));
            evt.Descriptor.GetAddMethod().Invoke(evt.OwningControl, new object[] { handler });

            control.Visible = iCommand.CanExecute(this);

            //listen to canExecuteChanged
            iCommand.CanExecuteChanged += new EventHandler((s,e) => control.Visible = iCommand.CanExecute(this));
        }

        private void RebindCommands()
        {
            if (CommandStorage != null)
            {
                foreach (BindingDef binding in CommandStorage)
                {
                    ExecuteCommandBind(binding);
                }
            }
        }
        
        private bool DetermineIfCollection(IBindingTarget control, object value)
        {
            if (value is IEnumerable && !(value is string))
                return true;

            return false;
        }

        private BindingMode DetermineBindingDirection(IBindingTarget control, object value)
        {
            if (control == null)
                return BindingMode.Unsupported;

            //String are IEnumerable!
            if (DetermineIfCollection(control, value))
                return BindingMode.OneWay;

            return BindingMode.TwoWay;
        }

        #endregion Bind

        #region Unbind


        /// <summary>
        /// Called by the binder to attempt the bind/unbind process
        /// </summary>
        private void ExecuteStagedUnbind()
        {
            try
            {
                if (!bindingContainer.IsPostBack)
                {
                    //If it's NOT a postback, always attempt a bind
                    performBind = true;
                }
                else
                {
                    //unbind if
                    if (this.updateSourceTrigger == UpdateSourceTrigger.PostBack)
                    {
                        ExecuteUnbind();
                    }
                }

                AttemptBind();
                RebindCommands();
                
            }
            finally
            {
                performBind = false;
            }

        }

        /// <summary>
        /// Call this method to initiate an explicit manual unbind
        /// </summary>
        public void ExecuteUnbind()
        {
            EnsureBindingSerivces();
            PushDataToModel();
            AttemptBind();
        }

        private void AttemptBind()
        { 
            
            //(Re)binds will occur on the intial load and if changes are detected
            // on the underlying model either as a result of an unbind operation (to support, cascading)
            // or as a direct model update.
            if (performBind)
            {
                performBind = false;
                this.DataStorage.Clear();
                this.CommandStorage.Clear();
                //will cause commands to be rebound as well
                this.bindingContainer.DataBind();
            }
        
        }

        private void bindingContainer_Load(object sender, EventArgs e)
        {
            EnsureBindingSerivces();
            ExecuteStagedUnbind();
        }

        private void PushDataToModel()
        {
            isPushing = true;
            try
            {

               
                //Listen for changes if the context implements INotifyPropertyChanged
                INotifyPropertyChanged notifyContext = this.DataContext as INotifyPropertyChanged;
                if (notifyContext != null)
                    notifyContext.PropertyChanged += new PropertyChangedEventHandler(DataItem_PropertyChanged);

                //Load all data items 
                //this.DataStorage.LoadDataItems(this.bindingContainer);

                //this works fine if all bindings are relative to the view model (ie, we only 
                //ever have one data context....if multiple contexts are to be supported
                //then we will first have to group by data context and then by source expression
                if (this.DataStorage != null)
                {
                    var groupBindings = this.DataStorage
                                .Where(x => x.BindingOptions.Mode == BindingMode.TwoWay)
                                .GroupBy(x => x.SourceExpression.FullyQualified);

                    foreach (var grouping in groupBindings)
                    {
                        var authorative = grouping.Where(x => x.BindingOptions.IsAuthorative).FirstOrDefault();
                        if (authorative != null)
                        {
                            ApplyBinding(authorative, this.bindingContainer);
                        }
                        else
                        {
                            //applying bindings in the order they were created
                            //the last bindings value will be the ultimate value of the source 
                            //unless the source implements a precedence/validation mechanism
                            List<BindingDef> nonAuthorative = grouping.ToList();
                            foreach (BindingDef binding in nonAuthorative)
                            {
                                ApplyBinding(binding, this.bindingContainer);
                            }
                        }
                    }
                }

            }
            finally
            {
                isPushing = false;
            }
        }

        private void ApplyBinding(BindingDef binding, IBindingContainer bindingContainer)
        {
            TargetProperty property = binding.ResolveAsTarget(this.bindingContainer);
            if (property != null)
            {
                object value = property.Value;
                SetProperty(binding, value);
            }
        }

        //TODO: Should probably be a member of binding.
        private void SetProperty(BindingDef binding, object value)
        {

            SourceProperty property = binding.SourceExpression.ResolveAsSource(this.DataContext);

            if (property != null && property.Descriptor != null && !property.Descriptor.IsReadOnly)
            {
                object convertedValue = null;
                Type destinationType = property.Descriptor.PropertyType;

                if (binding.HasValueConverter)
                {
                    IValueConverter valueConverter = binding.GetValueConverterInstance();

                    convertedValue = valueConverter.ConvertBack(value, property.Descriptor.PropertyType, null, null);
                }
                else
                {
                    if (TypeHelper.IsSimpleType(value.GetType()) && TypeHelper.IsSimpleType(destinationType))
                        convertedValue = Convert.ChangeType(value, destinationType);

                    else
                    {
                        TypeConverter converter = TypeDescriptor.GetConverter(destinationType);
                        if (converter != null)
                            convertedValue = converter.ConvertFrom(value);
                        else
                        {
                            converter = TypeDescriptor.GetConverter(value.GetType());

                            if (converter != null)
                                convertedValue = converter.ConvertTo(value, destinationType);
                        }
                    }

                }

                property.Descriptor.SetValue(property.OwningInstance, convertedValue);
            }
        }

        private object AttemptConvert(BindingDef binding, object value, IBindingTarget container)
        {
            if (binding.HasValueConverter)
            {
                IValueConverter converter = binding.GetValueConverterInstance();
                value = converter.Convert(value, binding.ResolveAsTarget(container).Descriptor.PropertyType, binding, CultureInfo.CurrentCulture);
            }

            return value;

        }

        #endregion 

        #region Virtual / Abstract / Misc

        protected virtual void EnsureBindingSerivces()
        {
            foreach (BindingDef binding in this.DataStorage.Union(this.CommandStorage))
                binding.ControlService = controlService;
        }

        private void DataItem_PropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            if (isPushing)
                performBind = true;
            else
                bindingContainer.DataBind();
        }

        #endregion 
    }
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer
United Kingdom United Kingdom
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions