Search code examples
c#wpfxamlvisual-studio-extensions

Can't successfully bind ObservableCollection of Tuples to XAML (VS Extension)


Having a hard time binding an ObservableCollection of Tuples to XAML in a Visual Studio 2022 extension.

I'm at least partially trying to follow this tutorial, though it's not quite doing the same thing.

  • I set up an ObservableCollection property of named tuples.
  • I initialize that collection with two such tuples.
  • I set DataContext to this of my extension's UserControl per tutorial:
    • "DataContext must be an object, this object is the “ViewModel” of our GUI and all the data bindings will be done on this object. If the data we use are not all combined into one class but exist as our MainWindow’s properties, we set the DataContext to be the MainWindow itself:"
  • I set the ItemsSource of my ItemsControl in the C#.
  • I bind to properties of the named Tuple (which seems like fair game) in the XAML.
    • <Checkbox Foreground="LightGreen" Content="{Binding Name}" IsChecked="{Binding IsChecked}"></CheckBox>

Result:

I get two CheckBoxes with no Content.

screenshot of two checkboxes with no labels

C#

    public partial class MyWindowControl : UserControl
    {

        private ObservableCollection<(string Name, bool IsChecked)> ChutzpahItems { get; set; } =
            new ObservableCollection<(string Name, bool IsChecked)>()
            {
                ("Thy Smy Name", true),
                ("Thy Smy OtherName", true),
            };

        /// <summary>
        /// Initializes a new instance of the <see cref="MyWindowControl"/> class.
        /// </summary>
        public MyWindowControl()
        {
            DataContext = this;
            this.InitializeComponent();
            this.itcChutzpah.ItemsSource = ChutzpahItems;
            CheckForPrereqs();
        }
// ...

XAML

<UserControl x:Class="MyWindowExtension.MyWindowControl"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:vsshell="clr-namespace:Microsoft.VisualStudio.Shell;assembly=Microsoft.VisualStudio.Shell.15.0"
             mc:Ignorable="d"
             d:DesignHeight="300" d:DesignWidth="300"

             Name="MyToolWindow">

    <Grid>
        <DockPanel>
            <StackPanel
                DockPanel.Dock="Top"
                Orientation="Vertical"
            >
<!-- some other widget stuff removed. -->

                <ItemsControl Name="itcChutzpah">
                    <ItemsControl.ItemTemplate>
                        <DataTemplate>
                            <StackPanel>
                                <CheckBox
                                    Foreground="LightGreen"
                                    Content="{Binding Name}"
                                    IsChecked="{Binding IsChecked}"
                                ></CheckBox>
                            </StackPanel>
                        </DataTemplate>
                    </ItemsControl.ItemTemplate>
                </ItemsControl>
            </StackPanel>

<!-- some other widget stuff removed. -->
        </DockPanel>
    </Grid>
</UserControl>

I mean pretty clearly I AM getting a bound collection, but my bindings have the wrong path or something.

Two questions:

  1. What am I doing wrong?
  2. How should I have debugged this?


Solution

  • Going to put @Yarik's comment as a wiki answer here. @Clemens also was pointing towards the same answer.

    If either wants to add an answer, I'll delete this one.


    Debugging bindings

    As both @Yarik and @Clemens mentioned, Debug >>> Windows >>> XAML Binding Failures is a good source for debugging.

    That window gives us this, which shows we aren't binding because Name & IsChecked aren't properties, just as the comments warned.

    Severity    Count   Data Context    Binding Path    Target              Target Type    Description    
    Error       2       ValueTuple`2    Name            CheckBox.Content    Object         Name property not found on object of type ValueTuple`2.
    Error       2       ValueTuple`2    IsChecked       CheckBox.IsChecked  Nullable`1     IsChecked property not found on object of type ValueTuple`2.         
    

    ValueTuple vs. Tuple

    As @Yarik and @Clemens mention:

    From the documentation of tuple types it seems that the names generate fields, not properties.

    and

    Tuple<,> is not the same as ValueTuple<,>. The first one is a class with properties (so it's ok to use with binding), the second one is a struct with fields.

    And since XAML needs properties to bind to, the ValueTuple isn't going to work.

    No, really, you can't use ValueTuples

    This means that simply taking away the names, expecting the behavior to match what's discussed over at Bind wpf listbox to ObservableCollection of tuples simply because you have Item1 and Item2 won't work either. It's still a value tuple.

    Which means that...

    private ObservableCollection<(string, bool)> ChutzpahItems { get; set; } =
        new ObservableCollection<(string, bool)>()
        {
            ("Thy Smy Name", true),
            ("Thy Smy OtherName", true),
        };
    

    and

    <CheckBox
        Foreground="LightGreen"
        Content="{Binding Item1}"
        IsChecked="{Binding Item2}"
    ></CheckBox>
    

    ... gives the same sorts of errors.

    Severity    Count   Data Context    Binding Path    Target              Target Type   Description
    Error           4   ValueTuple`2    Item1           CheckBox.Content    Object        Item1 property not found on object of type ValueTuple`2.          
    Error           4   ValueTuple`2    Item2           CheckBox.IsChecked  Nullable`1    Item2 property not found on object of type ValueTuple`2.          
    

    Using an old school Tuple

    Using old school Tuple<string, bool> does work, (one caveat and an explanation) just as @Yarik & @Clements predicted.

    private ObservableCollection<Tuple<string, bool>> ChutzpahItems { get; set; } =
        new ObservableCollection<Tuple<string, bool>>()
        {
            new Tuple<string, bool>("Thy Smy Name", true),
            new Tuple<string, bool>("Thy Smy OtherName", true),
        };
    
    <CheckBox
        Foreground="LightGreen"
        Content="{Binding Item1}"
        IsChecked="{Binding Item2, Mode=OneWay}"
    ></CheckBox>
    

    working after swapping to true/non-value tuples

    And there you have it.

    There's a better way

    Better, of course, would be to create a class that

    1. Has named properties and
    2. Isn't read-only like a Tuple.
    public class CheckboxInfo
    {
        public string Name { get; set; }
        public bool IsChecked { get; set; }
    
        public CheckboxInfo(string name, bool isChecked)
        {
            Name = name;
            IsChecked = isChecked;
        }
    }
    

    Which just goes to show that "Sometimes "just the data" is the Right Thing", but, and I think Skeet would agree, most of the time it's not.