-2

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.
Simon Mourier
  • 132,049
  • 21
  • 248
  • 298
Mason Wheeler
  • 82,511
  • 50
  • 270
  • 477
  • Where are your bindings? – mm8 Sep 07 '21 at 14:22
  • @mm8 In the XAML, exactly where you'd expect. – Mason Wheeler Sep 07 '21 at 14:51
  • Appearantly not where expected if the bindings don't work. You haven't posted your XAML... – mm8 Sep 07 '21 at 14:51
  • @mm8 Which XAML am I supposed to post? As I said, this is a consistent problem across all the property grids I've tried. I know that the way I'm writing the bindings is not the problem, because some classes do bind properly. (Exactly which ones do or don't varies from one grid to another, though.) I'm not looking for help with debugging a binding to one grid; I'm *specifically* not looking for that and that should have been clear from the post. I'm looking to understand what the root cause is behind a general problem that affects many different grids. – Mason Wheeler Sep 07 '21 at 14:57
  • Are you saying asking or thinking that there is a problem with "all the property grids" you have tried? Or with the `DbConnectionStringBuilder`? Or do you think there is some issue with your implementation? Either way, how should anyone be able to answer your question given the (lack of) details you have provided in the question? – mm8 Sep 07 '21 at 15:02
  • @mm8 Yes, that is exactly what I said in my post: "every last one of them had the same problem." And I gave plenty of information on how to reproduce it, including the code of a sample class that causes the problem on every grid. If there's an actual problem with my post, by all means call it out, but don't go claiming there are problems that aren't actually there. – Mason Wheeler Sep 07 '21 at 15:07
  • Yes, DbConnectionStringBuilder is "special", it implements ICustomTypeDescriptor (https://learn.microsoft.com/en-us/dotnet/api/system.data.common.dbconnectionstringbuilder#remarks) which is used by all property grids. Your methods will never be called, that's by design, they're just used by DbConnectionStringBuilder as metadata descriptors. But that shouldn't be a problem, just override `this[string keyword]` to raise property changed events, if that's what you're after. – Simon Mourier Sep 09 '21 at 06:21
  • PS: I mean, all property grids use TypeDescriptors, and therefore gracefully support ICustomTypeDescriptor. – Simon Mourier Sep 09 '21 at 06:28
  • @SimonMourier OK, that's kind of helpful. It doesn't explain why some classes bind gracefully and edit successfully and some don't, and why which ones work and which fail varies from one grid to the next, though. – Mason Wheeler Sep 09 '21 at 12:05
  • 2
    It explains it all, but maybe you don't understand. I'm not sure what you mean by "bind gracefully and edit successfully". PropertyGrids bind with a TypeDescriptor. Since DbConnectionStringBuilder implements ICustomTypeDescriptor, it binds with the properties that ICustomTypeDescriptor exposes through GetProperties, not with the .NET/C# compiled properties. It's very similar to a WPF control dependency properties vs .NET/C# properties which are not used when binding). Now it's possible some property grids don't bind, or don't use TypeDescriptor or don't work. – Simon Mourier Sep 09 '21 at 14:31
  • @SimonMourier Blargh, that's the first thing anyone's said that's made any sense. After a bunch of source diving, I found [the source of those nonsensical null values](https://github.com/microsoft/referencesource/blob/master/System.Data/System/Data/Common/DbConnectionStringCommon.cs#L102). That helps get things a bit closer, but it still doesn't explain why properties that *are* set show up as blank/null. – Mason Wheeler Sep 09 '21 at 15:33
  • That maybe a problem with the property grid you use. I use this one I wrote (years ago): https://github.com/SoftFluent/SoftFluent.Windows#propertygrid with a bit of code added to your class so it supports WPF binding: https://pastebin.com/raw/hHTSa84G everything seems to work fine including error handling – Simon Mourier Sep 09 '21 at 16:07
  • @SimonMourier Oh, you're the author of SoftFluent? Yeah, that was one of the various property grids I tried. And I can change my example code easily enough, but that won't fix it for existing third-party classes like [the MySql one](https://github.com/mysql-net/MySqlConnector/blob/master/src/MySqlConnector/MySqlConnectionStringBuilder.cs), which appears to be the only one from my test corpus that your grid is failing on. (Making it better than the commercial ones I tested, BTW. Congrats on that!) Any way to make this work out-of-the-box? – Mason Wheeler Sep 09 '21 at 16:22
  • The grid doesn't "fail", it just can't do much more w/o custom code. Not every .NET class works fine with WPF binding, this is the original reason why Microsoft "invented" MVVM (and added an extra bindable layer), because WPF can't live w/o binding. This MySql one clearly doesn't work with WPF (and it's not really working with Winforms' one either). Plus it's sealed which is a bad design decision. – Simon Mourier Sep 09 '21 at 16:53
  • @SimonMourier OK, thanks. As we all know by sad experience, comments tend to be rather ephemeral around here. If you can sum this information up in an answer, I'll accept it. – Mason Wheeler Sep 09 '21 at 17:19
  • "Seal classes unless they're designed for extensibility" is a design decision, but it's not a bad one. – Bradley Grainger Sep 09 '21 at 19:02
  • @BradleyGrainger Depends on the definition you're using. Personally, I consider it an act of severe arrogance. From the point of view of someone who needs to inherit from it, it looks like "I can't see any good reason why this class should need to be extended, therefore I proclaim that no such reason can ever exist." – Mason Wheeler Sep 09 '21 at 19:08
  • I'm in the "seal unless designed for inheritance" camp, which is more of a statement about API design and backwards compatibility than "severe arrogance"; cf https://stackoverflow.com/a/37346728/23633. (One would also have to make all methods `virtual` to support arbitrary inheritance. Again, not arrogant to not do so, but pragmatic that any change is likely now a _breaking_ change.) – Bradley Grainger Sep 09 '21 at 19:13
  • Is the fundamental problem here that some `DbConnectionStringBuilder`-derived classes don't implement `TryGetValue` properly? – Bradley Grainger Sep 09 '21 at 19:13
  • I'm not sure. Maybe @SimonMourier would know? If I understood all this WPF binding mumbo-jumbo in enough depth to debug it myself, I wouldn't have needed to post a question here and then put a bounty on it! – Mason Wheeler Sep 09 '21 at 19:18
  • @BradleyGrainger - I'm also in the "seal unless designed for inheritance" camp but this is not the case here as DbConnectionStringBuilder is totally designed for inheritance, it's in fact its main feature. – Simon Mourier Sep 11 '21 at 06:41
  • @SimonMourier Sure, `DbConnectionStringBuilder` was designed for inheritance (kinda), but `MySqlConnectionStringBuilder` wasn't. I added no methods for inheritors to use to safely update its state. I put no thought into designing a `protected` API. I wrote no documentation on how to safely/correctly subclass it. (Neither did the ADO.NET authors, which is partly why I got the `MySqlConnectionStringBuilder` implementation wrong.) Since I did none of that, I sealed the class. – Bradley Grainger Sep 11 '21 at 15:44
  • 1
    @BradleyGrainger - yes, that's a convoluted way to say "seal unless designed for inheritance" :-) and yes, all Microsoft's provided classes are sealed, and I would address the same remark at their classes, as DbConnectionStringBuilder is really designed for inheritance (it could even be abstract). – Simon Mourier Sep 11 '21 at 19:01

1 Answers1

3

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...

Simon Mourier
  • 132,049
  • 21
  • 248
  • 298