Search code examples
wpfdata-bindingpropertygrid

Why do DbConnectionStringBuilder descendants break Property Grid binding so thoroughly?


I'm trying to set up a way to configure a database connection. I found a simple property grid on GitHub, added it to my project, and bound a DbConnectionStringBuilder descendant to it, and it immediately broke. It found the names and types of all the properties, but it appears to not actually be linked to the object instance, so all the properties show null values and attempting to edit them causes various problems.

After filing a bug report with the developer got me nowhere, I tried about half a dozen other property grids, both free and (demo versions of) commercial offerings, and every last one of them had the same problem! Some connection string builders work just fine, others break, sometimes it's different for different grids, but none of them binds correctly to the entire set of 5 I'm using, for SQL Server, Postgres, Firebird, MySQL, and a very simple test case class I developed just to repro this. (In fact, my simple test case for connecting to a "CSV database" was the only one that broke all of them!)

Is there something particularly weird about DbConnectionStringBuilder that WPF data binding is allergic to?

Repro case if anyone wants to try it. CSV database configuration class:

using System;
using System.ComponentModel;
using System.Data.Common;

namespace Repro
{
    public class CsvConfigurator : DbConnectionStringBuilder
    {
        public CsvConfigurator() { }

        public CsvConfigurator(string conf)
        {
            ConnectionString = conf;
        }

        public string Delimiter
        {
            get => GetString(nameof(Delimiter));
            set => this[nameof(Delimiter)] = value;
        }

        public bool AutoDetectDelimiter
        {
            get => GetBool(nameof(AutoDetectDelimiter));
            set => this[nameof(AutoDetectDelimiter)] = value;
        }

        public bool UsesHeader
        {
            get => GetBool(nameof(UsesHeader));
            set => this[nameof(UsesHeader)] = value;
        }

        public bool UsesQuotes
        {
            get => GetBool(nameof(UsesQuotes));
            set => this[nameof(UsesQuotes)] = value;
        }

        public char QuoteChar
        {
            get => GetChar(nameof(QuoteChar), '"');
            set => this[nameof(QuoteChar)] = value;
        }

        public char EscapeChar
        {
            get => GetChar(nameof(EscapeChar), '\\');
            set => this[nameof(EscapeChar)] = value;
        }

        protected string GetString(string key) => TryGetValue(key, out var value) ? (string)value : null;
        protected bool GetBool(string key) => TryGetValue(key, out var value) ? Convert.ToBoolean(value) : false;
        protected char GetChar(string key, char defaultValue)
        {
            var result = GetString(key);
            return string.IsNullOrEmpty(result) ? defaultValue : result[0];
        }
    }
}
  1. Create a project with a property grid -- any property grid -- on it.
  2. Instantiate the above class.
  3. Bind it to the grid as the object to be inspected.
  4. Put breakpoints on the getters and setters so you can see when the data binding is actually happening.
  5. Watch everything not actually be bound. (Look at the checkbox controls and how they're in a null state despite the properties in question not being bool? type.)
  6. Do some editing.
  7. Watch the breakpoints not get hit.

Solution

  • Yes, there's a reason why a class deriving from DbConnectionStringBuilder has a special behavior when used with a property grid.

    It's because it implements the ICustomTypeDescriptor Interface. In general, a property grid uses the TypeDescriptor.GetProperties method which will by default defer to ICustomTypeDescriptor if implemented.

    What it means is a property grid will not use the compiled .NET/C# properties to represent an instance, but instead use the properties from the ICustomTypeDescriptor interface, with custom PropertyDescriptor instances.

    So the compiled .NET/C# properties will not be used at all by a property grid, only "virtual" properties made up by the internal DbConnectionStringBuilder code (you can check its code here https://github.com/microsoft/referencesource/blob/master/System.Data/System/Data/Common/DbConnectionStringBuilder.cs#L335). These "virtual" properties will be constructed using .NET compiled ones, but their code won't be used for getting or setting them.

    This is in a way similar to WPF dependency properties feature where the compiled properties of a .NET class are just used by .NET code, not by the WPF binding/XAML engine (except WPF uses DependencyProperty.Register code to define the dependency properties, not the compiled ones).

    If you want to support the WPF binding engine, you can implement INotifyPropertyChanged to your class like this for example:

    public event PropertyChangedEventHandler PropertyChanged;
    
    // thanks to how DbConnectionStringBuilder is designed,
    // we can override this central method
    public override object this[string keyword]
    {
        get => base[keyword];
        set
        {
            object existing = null;
            try
            {
                existing = base[keyword];
            }
            catch
            {
                // do nothing
            }
    
            if (existing == null)
            {
                if (value == null)
                    return;
            }
            else if (existing.Equals(value))
                return;
    
            base[keyword] = value;
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(keyword));
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ConnectionString)));
        }
    }
    

    For existing classes such as this one: MySqlConnectionStringBuilder, there's nothing you can do (unless wrapping them with another class implementing ICustomTypeDescriptor with an approach similar to this DynamicTypeDescriptor). Not every .NET class works fine with WPF binding or even the standard Winforms binding. And it's sealed...