File: SystemDataExtension.cs
Web Access
Project: src\src\Microsoft.DotNet.Wpf\src\Extensions\PresentationFramework-SystemData\PresentationFramework-SystemData.csproj (PresentationFramework-SystemData)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
// Description: Helper methods for code that uses types from System.Data.
 
using System;
using System.ComponentModel;
using System.Data;
using System.Data.SqlTypes;
using System.Diagnostics.CodeAnalysis;
 
namespace MS.Internal
{
    //FxCop can't tell that this class is instantiated via reflection, so suppress the FxCop warning.
    [SuppressMessage("Microsoft.Performance","CA1812:AvoidUninstantiatedInternalClasses")]
    internal class SystemDataExtension : SystemDataExtensionMethods
    {
        // return true if the list is a DataView
        internal override bool IsDataView(IBindingList list)
        {
            return (list is DataView);
        }
 
        // return true if the item is a DataRowView
        internal override bool IsDataRowView(object item)
        {
            return (item is DataRowView);
        }
 
        // return true if the value is null in the SqlTypes sense
        internal override bool IsSqlNull(object value)
        {
            INullable nullable = value as INullable;
            return (nullable != null && nullable.IsNull);
        }
 
        // return true if the type is nullable in the SqlTypes sense
        internal override bool IsSqlNullableType(Type type)
        {
            return typeof(INullable).IsAssignableFrom(type);
        }
 
        // ADO DataSet exposes some properties that cause problems involving
        // identity and change notifications.  We handle these specially.
        internal override bool IsDataSetCollectionProperty(PropertyDescriptor pd)
        {
            if (s_DataTablePropertyDescriptorType == null)
            {
                // lazy load the types for the offending PD's.  They're internal, so
                // we get them indirectly.
                DataSet dataset = new DataSet();
                dataset.Locale = System.Globalization.CultureInfo.InvariantCulture;
 
                DataTable table1 = new DataTable("Table1");
                table1.Locale = System.Globalization.CultureInfo.InvariantCulture;
                table1.Columns.Add("ID", typeof(int));
                dataset.Tables.Add(table1);
 
                DataTable table2 = new DataTable("Table2");
                table2.Locale = System.Globalization.CultureInfo.InvariantCulture;
                table2.Columns.Add("ID", typeof(int));
                dataset.Tables.Add(table2);
 
                dataset.Relations.Add(new DataRelation("IDRelation",
                                                    table1.Columns["ID"],
                                                    table2.Columns["ID"]));
 
                System.Collections.IList list = ((IListSource)dataset).GetList();
                PropertyDescriptorCollection pdc = TypeDescriptor.GetProperties(list[0]);
                s_DataTablePropertyDescriptorType = pdc["Table1"].GetType();
 
                pdc = ((ITypedList)table1.DefaultView).GetItemProperties(null);
                s_DataRelationPropertyDescriptorType = pdc["IDRelation"].GetType();
            }
 
            Type pdType = pd.GetType();
            return (pdType == s_DataTablePropertyDescriptorType) ||
                (pdType == s_DataRelationPropertyDescriptorType);
        }
 
        // Intercept GetValue calls for certain ADO properties
        internal override object GetValue(object item, PropertyDescriptor pd, bool useFollowParent)
        {
            object value = GetRawValue(item, pd, useFollowParent);
 
            if (IsDataSetCollectionProperty(pd))
            {
                // ADO returns a newly-created object (of type DataView or RelatedView)
                // for each call to the getter of a "DataSet collection property".
                // These objects are not referenced by any other ADO objects,
                // so it is up to the caller to keep them alive.  But WPF tries
                // hard not to add strong references to these objects.  There
                // are only three ways:
                //  1. Creating a BindingListCollectionView over the object
                //  2. Adding a ValueChanged listener for one of the object's properties
                //  3. Caching the object in the ValueTable
                //
                // Actually (3) no longer happens at all - it was causing a memory
                // leak of the entire DataSet. 
                //
                // If the app never uses (1) or (2), there's nothing to keep the
                // object alive.  There is a case where this actually
                // happens - the app uses bindings with indexers on the object, but
                // doesn't use any collection views or PropertyDescriptor-backed
                // properties.   After a while, the object is GC'd, and the app
                // silently stops working.
                //
                // To fix this, we add a reference from a suitable ADO object to
                // the new DataView/RelatedView.  This reference can't involve
                // any WPF objects - that would bring back the memory leak.
                // Instead, we use an ephemeral object created just for this purpose.
                // The ephemeral object subscribes to an event on the "suitable
                // ADO object", intentionally *not* using the WeakEvent pattern
                // so that the ADO object refers to the ephemeral object via the handler.
                // The ephemeral object also holds a strong reference to the new
                // DataView/RelatedView.  Together, these references tie the
                // lifetime of the DataView/RelatedView to the lifetime of the
                // ADO object, as desired.
 
                if (pd.GetType() == s_DataTablePropertyDescriptorType)
                {
                    // the "suitable ADO object" is the corresponding DataTable
                    DataTable dataTable = (value as DataView)?.Table;
                    if (dataTable != null)
                    {
                        new DataTableToDataViewLink(dataTable, value);
                    }
                }
                else if (pd.GetType() == s_DataRelationPropertyDescriptorType)
                {
                    // the "suitable ADO object" is the parent DataRowView
                    DataRowView dataRowView = item as DataRowView;
                    if (dataRowView != null)
                    {
                        new DataRowViewToRelatedViewLink(dataRowView, value);
                    }
                }
            }
 
            return value;
        }
 
        private object GetRawValue(object item, PropertyDescriptor pd, bool useFollowParent)
        {
            if (useFollowParent && pd.GetType() == s_DataRelationPropertyDescriptorType)
            {
                // the DataRelation property returns a child view that doesn't
                // work in master/detail scenarios, when the primary key linking
                // the two tables changes. 
                // ADO added a new method that returns a better child view, specifically
                // to fix this bug, but System.Data.DataRelationPropertyDescriptor.GetValue
                // still uses the old method:
                //
                // public override object GetValue(object component) {
                //     DataRowView dataRowView = (DataRowView) component;
                //     return dataRowView.CreateChildView(relation);
                // }
                //
                // so we intercept the GetValue call and use the new method.
                // (The value of the 'relation' member isn't publicly visible,
                // but its name is.  That's enough to call the new method.)
                DataRowView dataRowView = (DataRowView)item;
                return dataRowView.CreateChildView(pd.Name, followParent:true);
            }
 
            // otherwise, call GetValue the normal way
            return pd.GetValue(item);
        }
 
        // return true if DBNull is a valid value for the given item and column.
        // The column may be specified directly by name, or indirectly by indexer: Item[arg]
        internal override bool DetermineWhetherDBNullIsValid(object item, string columnName, object arg)
        {
            DataRowView drv;
            DataRow dr = null;
            if ((drv = item as DataRowView) == null &&
                (dr = item as DataRow) == null)
            {
                return false;
            }
 
            // this code was provided by the ADO team
            DataTable table = (drv != null) ? drv.DataView.Table : dr.Table;
 
            DataColumn column = null;
            if (arg != null)
            {
                if ((columnName = arg as String) != null)
                {
                    column = table.Columns[columnName];
                }
                else if (arg is int)
                {
                    int index = (int)arg;
                    if (0 <= index && index < table.Columns.Count)
                    {
                        column = table.Columns[index];
                    }
                }
            }
            else if (columnName != null)
            {
                column = table.Columns[columnName];
            }
 
            return (column != null) && column.AllowDBNull;
        }
 
        private class DataTableToDataViewLink
        {
            public DataTableToDataViewLink(DataTable dataTable, object target)
            {
                _target = target;
                dataTable.Initialized += OnInitialized;
            }
 
            void OnInitialized(object sender, EventArgs e)
            {
            }
 
            object _target;
        }
 
        private class DataRowViewToRelatedViewLink
        {
            public DataRowViewToRelatedViewLink(DataRowView dataRowView, object target)
            {
                _target = target;
                dataRowView.PropertyChanged += OnPropertyChanged;
            }
 
            void OnPropertyChanged(object sender, PropertyChangedEventArgs e)
            {
            }
 
            object _target;
        }
 
        static Type s_DataTablePropertyDescriptorType;
        static Type s_DataRelationPropertyDescriptorType;
    }
}