File: Base\System\Windows\Data\DataSourceProvider.cs
Project: wpf\src\WindowsBase.csproj (WindowsBase)
//---------------------------------------------------------------------------
//
// <copyright file="DataSourceProvider.cs" company="Microsoft">
//    Copyright (C) Microsoft Corporation.  All rights reserved.
// </copyright>
//
// Description: common base class and contract for data source provider objects
//
// Specs:       http://avalon/connecteddata/Specs/Avalon%20DataProviders.mht
//
//---------------------------------------------------------------------------
 
using System;
using System.Diagnostics;
using System.ComponentModel;
using System.Threading;
using System.Windows.Threading;     // Dispatcher*
 
using MS.Internal;  // Invariant
 
namespace System.Windows.Data
{
 
    /// <summary>
    /// Common base class and contract for data source providers.
    /// A DataProvider in Avalon is the factory that executes some query
    /// to produce a single object or a list of objects that can be used
    /// as sources for Avalon data bindings.
    /// It is a convenience wrapper around existing data model, it does not replace any data model.
    /// A data provider does not attempt to condense the complexity and versatility of a data model
    /// like ADO into one single object with a few properties.
    /// </summary>
    /// <remarks>
    /// DataSourceProvider is an abstract class and cannot directly be used as a data provider.
    /// Use one of the derived concrete provider, e.g. XmlDataProvider, ObjectDataProvider.
    /// The DataProvider aware of Avalon's threading and dispatcher model. The data provider assumes
    /// the thread at creation time to be the UI thread. Events will get marshalled from a worker thread
    /// to the app's UI thread.
    /// </remarks>
    public abstract class DataSourceProvider : INotifyPropertyChanged, ISupportInitialize
    {
        /// <summary>
        /// constructor captures the Dispatcher associated with the current thread
        /// </summary>
        protected DataSourceProvider()
        {
            _dispatcher = Dispatcher.CurrentDispatcher;
        }
 
        /// <summary>
        /// Start the initial query to the underlying data model.
        /// The result will be returned on the Data property.
        /// This method is typically called by the binding engine when
        /// dependent data bindings are activated.
        /// Set IsInitialLoadEnabled = false to prevent or delay the automatic loading of data.
        /// </summary>
        /// <remarks>
        /// The InitialLoad method can be called multiple times.
        /// The provider is expected to ignore subsequent calls once the provider
        /// is busy executing the initial query, i.e. the provider shall not restart
        /// an already running query when InitialLoad is called again.
        /// When the query finishes successfully, any InitialLoad call will still not re-query data.
        /// The InitialLoad operation is typically asynchronous, a DataChanged event will
        /// be raised when the Data property assumed a new value.
        /// The application should call Refresh to cause a refresh of data.
        /// </remarks>
        public void InitialLoad()
        {
            // ignore call if IsInitialLoadEnabled == false or already started initialization
            if (!IsInitialLoadEnabled || _initialLoadCalled)
                return;
 
            _initialLoadCalled = true;
            BeginQuery();
        }
 
        /// <summary>
        /// Initiates a Refresh Operation to the underlying data model.
        /// The result will be returned on the Data property.
        /// </summary>
        /// <remarks>
        /// A refresh operation is typically asynchronous, a DataChanged event will
        /// be raised when the Data property assumed a new value.
        /// If the refresh operation fails, the Data property will be set to null;
        /// the Error property will be set with the error exception.
        /// The app can call Refresh while a previous refresh is still underway.
        /// Calling Refresh twice will cause the DataChanged event to raise twice.
        /// </remarks>
        public void Refresh()
        {
            _initialLoadCalled = true;
            BeginQuery();
        }
 
        /// <summary>
        /// Set IsInitialLoadEnabled = false to prevent or delay the automatic loading of data.
        /// </summary>
        [DefaultValue(true)]
        public bool IsInitialLoadEnabled
        {
            get { return _isInitialLoadEnabled; }
            set
            {
                _isInitialLoadEnabled = value;
                OnPropertyChanged(new PropertyChangedEventArgs("IsInitialLoadEnabled"));
            }
        }
 
        /// <summary>
        /// Get the underlying data object.
        /// This is the resulting data source the data provider
        /// </summary>
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
        public object Data
        {
            get { return _data; }
        }
 
        /// <summary>
        /// Raise this event when a new data object becomes available
        /// on the Data property.
        /// </summary>
        public event EventHandler DataChanged;
 
        /// <summary>
        /// Return the error of the last query operation.
        /// To indicate there was no error, it will return null
        /// </summary>
        public Exception Error
        {
            get { return _error; }
        }
 
 
        /// <summary>
        /// Enter a Defer Cycle.
        /// Defer cycles are used to coalesce property changes, any automatic
        /// Refresh is delayed until the Defer Cycle is exited.
        /// </summary>
        /// <remarks>
        /// most typical usage is with a using block to set multiple proporties
        /// without the automatic Refresh to occur
        /// <code>
        ///     XmlDataProvider xdv = new XmlDataProvider();
        ///     using(xdv.DeferRefresh()) {
        ///         xdv.Source = "http://foo.com/bar.xml";
        ///         xdv.XPath = "/Bla/Baz[@Boo='xyz']";
        ///     }
        /// </code>
        /// </remarks>
        public virtual IDisposable DeferRefresh()
        {
            ++_deferLevel;
            return new DeferHelper(this);
        }
 
 
        #region ISupportInitialize
        /// <summary>
        ///     Initialization of this element is about to begin
        /// </summary>
        void ISupportInitialize.BeginInit()
        {
            BeginInit();
        }
 
        /// <summary>
        ///     Initialization of this element has completed
        /// </summary>
        void ISupportInitialize.EndInit()
        {
            EndInit();
        }
        #endregion
 
 
        /// <summary>
        /// PropertyChanged event (per <see cref="INotifyPropertyChanged" />).
        /// </summary>
        event PropertyChangedEventHandler INotifyPropertyChanged.PropertyChanged
        {
            add
            {
                PropertyChanged += value;
            }
            remove
            {
                PropertyChanged -= value;
            }
        }
 
 
        //------------------------------------------------------
        //
        //  Protected Properties
        //
        //------------------------------------------------------
 
        /// <summary>
        /// IsRefreshDeferred returns true if there is still an
        /// outstanding DeferRefresh in use.  To get the best use
        /// out of refresh deferral, derived classes should try
        /// not to call Refresh when IsRefreshDeferred is true.
        /// </summary>
        protected bool IsRefreshDeferred
        {
            get
            {
                return (  (_deferLevel > 0)
                        || (!IsInitialLoadEnabled && !_initialLoadCalled));
            }
        }
 
        /// <summary>
        /// The current Dispatcher to the Avalon UI thread to use.
        /// </summary>
        /// <remarks>
        /// By default, this is the Dispatcher associated with the thread
        /// on which this DataProvider instance was created.
        /// </remarks>
        protected Dispatcher Dispatcher
        {
            get { return _dispatcher; }
            set
            {
                if (_dispatcher != value)
                {
                    _dispatcher = value;
                }
            }
        }
 
        //------------------------------------------------------
        //
        //  Protected Methods
        //
        //------------------------------------------------------
 
        #region Protected Methods
 
        /// <summary>
        /// Overridden by concrete data provider class.
        /// the base class will call this method when InitialLoad or Refresh
        /// has been called and will delay this call if refresh is deferred ot
        /// initial load is disabled.
        /// </summary>
        /// <remarks>
        /// The implementor can choose to execute the query on the same thread or
        /// on a background thread or using asynchronous API.
        /// When the query is complete, call OnQueryFinished to have the public properties updated.
        /// </remarks>
        protected virtual void BeginQuery()
        {
        }
 
        /// <summary>
        /// A concrete data provider will call this method
        /// to indicate that a query has finished.
        /// </summary>
        /// <remarks>
        /// This callback can be called from any thread, this implementation
        /// will marshal back the result to the UI thread
        /// before setting any of the public properties and before raising any events.
        /// <param name="newData">resulting data from query</param>
        /// </remarks>
        protected void OnQueryFinished(object newData)
        {
             OnQueryFinished(newData, null, null, null);
        }
 
        /// <summary>
        /// A concrete data provider will call this method
        /// to indicate that a query has finished.
        /// </summary>
        /// <remarks>
        /// This callback can be called from any thread, this implementation
        /// will marshal back the result to the UI thread
        /// before setting any of the public properties and before raising any events.
        /// <param name="newData">resulting data from query</param>
        /// <param name="error">error that occured while running query; null signals no error</param>
        /// <param name="completionWork">optional delegate to execute completion work on UI thread, e.g. setting additional properties</param>
        /// <param name="callbackArguments">optional arguments to send as parameter with the completionWork delegate</param>
        /// </remarks>
        protected virtual void OnQueryFinished(object newData, Exception error,
                                                DispatcherOperationCallback completionWork, object callbackArguments)
        {
            Invariant.Assert(Dispatcher != null);
            // check if we're already on the dispatcher thread
            if (Dispatcher.CheckAccess())
            {
                // already on UI thread
                UpdateWithNewResult(error, newData, completionWork, callbackArguments);
            }
            else
            {
                // marshal the result back to the main thread
                Dispatcher.BeginInvoke(
                    DispatcherPriority.Normal, UpdateWithNewResultCallback,
                    new object[] { this, error, newData, completionWork, callbackArguments });
            }
        }
 
        /// <summary>
        /// PropertyChanged event (per <see cref="INotifyPropertyChanged" />).
        /// </summary>
        protected virtual event PropertyChangedEventHandler PropertyChanged;
 
        /// <summary>
        /// Raises a PropertyChanged event (per <see cref="INotifyPropertyChanged" />).
        /// </summary>
        protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, e);
            }
        }
 
        /// <summary>
        ///     Initialization of this element is about to begin;
        ///     no implicit Refresh occurs until the matched EndInit is called
        /// </summary>
        protected virtual void BeginInit()
        {
            ++_deferLevel;
        }
 
        /// <summary>
        ///     Initialization of this element has completed;
        ///     this causes a Refresh if no other deferred refresh is outstanding
        /// </summary>
        protected virtual void EndInit()
        {
            EndDefer();
        }
 
        #endregion Protected Methods
 
        //------------------------------------------------------
        //
        //  Private Methods
        //
        //------------------------------------------------------
 
        #region Private Methods
 
        private void EndDefer()
        {
            Debug.Assert(_deferLevel > 0);
 
            --_deferLevel;
 
            if (_deferLevel == 0)
            {
                Refresh();
            }
        }
 
        private static object UpdateWithNewResult(object arg)
        {
            object[] args = (object[]) arg;
            Invariant.Assert(args.Length == 5);
            DataSourceProvider provider = (DataSourceProvider) args[0];
            Exception error = (Exception) args[1];
            object newData = args[2];
            DispatcherOperationCallback completionWork
                = (DispatcherOperationCallback) args[3];
            object callbackArgs = args[4];
 
            provider.UpdateWithNewResult(error, newData, completionWork, callbackArgs);
            return null;
        }
 
        private void UpdateWithNewResult(Exception error, object newData, DispatcherOperationCallback completionWork, object callbackArgs)
        {
            bool errorChanged = (_error != error);
            _error = error;
            if (error != null)
            {
                newData = null;
                _initialLoadCalled = false; // allow again InitialLoad after an error
            }
 
            _data = newData;
 
            if (completionWork != null)
                completionWork(callbackArgs);
 
            // notify any listeners
            OnPropertyChanged(new PropertyChangedEventArgs("Data"));
            if (DataChanged != null)
            {
                DataChanged(this, EventArgs.Empty);
            }
            if (errorChanged)
                OnPropertyChanged(new PropertyChangedEventArgs("Error"));
        }
 
        #endregion Private Methods
 
        //------------------------------------------------------
        //
        //  Private Types
        //
        //------------------------------------------------------
 
 
        #region Private Types
 
        private class DeferHelper : IDisposable
        {
            public DeferHelper(DataSourceProvider provider)
            {
                _provider = provider;
            }
 
            public void Dispose()
            {
                GC.SuppressFinalize(this);
                if (_provider != null)
                {
                    _provider.EndDefer();
                    _provider = null;
                }
            }
 
            private DataSourceProvider _provider;
        }
 
        #endregion
 
        //------------------------------------------------------
        //
        //  Private Fields
        //
        //------------------------------------------------------
        private bool _isInitialLoadEnabled = true;
        private bool _initialLoadCalled;
        private int _deferLevel;
        private object _data;
        private Exception _error;
        private Dispatcher _dispatcher;
 
        static readonly DispatcherOperationCallback UpdateWithNewResultCallback = new DispatcherOperationCallback(UpdateWithNewResult);
 
    }
 
}