| File: SystemDataExtension.cs | Web Access |
| Project: src\wpf\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. // 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 { Locale = System.Globalization.CultureInfo.InvariantCulture }; DataTable table1 = new DataTable("Table1") { Locale = System.Globalization.CultureInfo.InvariantCulture }; table1.Columns.Add("ID", typeof(int)); dataset.Tables.Add(table1); DataTable table2 = new DataTable("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 index) { 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; } private void OnInitialized(object sender, EventArgs e) { } private object _target; } private class DataRowViewToRelatedViewLink { public DataRowViewToRelatedViewLink(DataRowView dataRowView, object target) { _target = target; dataRowView.PropertyChanged += OnPropertyChanged; } private void OnPropertyChanged(object sender, PropertyChangedEventArgs e) { } private object _target; } private static Type s_DataTablePropertyDescriptorType; private static Type s_DataRelationPropertyDescriptorType; } }