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];
}
}
}
bool?
type.)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...