winui-3c++-winrt

How to bind to ComboBox.SelectedIndex using C++/WinRT and WinUI 3


I'm trying to bind an int to a ComboBox.SelectedIndex. Binding to a TextBox is trivial, but it appears that some extra plumbing is needed for the SelectedIndex. Here's the repro:

<!--MainWindow.xaml-->
<?xml version="1.0" encoding="utf-8"?>
<Window
    x:Class="MiniEditor.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:MiniEditor"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    Title="MiniEditor">

    <StackPanel Orientation="Vertical" HorizontalAlignment="Left" VerticalAlignment="Top">
        <ListView x:Name="MEList" Background="LightGoldenrodYellow" CanReorderItems="True" Height="600">
            <ListView.HeaderTemplate>
                <DataTemplate>
                    <Grid>
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition Width="30"/>
                            <ColumnDefinition Width="80"/>
                            <ColumnDefinition Width="80"/>
                        </Grid.ColumnDefinitions>
                        <TextBlock Grid.Column="0" Text="#" HorizontalAlignment="Center"/>
                        <TextBlock Grid.Column="1" Text="Name" HorizontalAlignment="Center"/>
                        <TextBlock Grid.Column="2" Text="Op" HorizontalAlignment="Center"/>
                    </Grid>
                </DataTemplate>
            </ListView.HeaderTemplate>
            <ListView.ItemTemplate>
                <DataTemplate x:DataType="local:OpDatWRC">
                    <Grid x:Name="MEGrid">
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition Width="30"/>
                            <ColumnDefinition Width="80"/>
                            <ColumnDefinition Width="80"/>
                        </Grid.ColumnDefinitions>
                        <TextBlock Grid.Column="0" Text="{x:Bind Path=opnum}" Width="30" VerticalAlignment="Center" HorizontalAlignment="Right"/>
                        <TextBox Grid.Column="1" Text="{x:Bind Path=name}"/>
                        <!--<ComboBox Grid.Column="2"  Width="80" SelectedIndex="{x:Bind Path=op, Mode=TwoWay}">-->
                        <ComboBox Grid.Column="2"  Width="80" SelectedIndex="{x:Bind Path=op, Mode=OneTime}">
                            <x:String>&lt;</x:String>
                            <x:String>&gt;</x:String>
                            <x:String>=</x:String>
                            <x:String>|&lt;|</x:String>
                            <x:String>|&gt;|</x:String>
                            <x:String>|=|</x:String>
                        </ComboBox>
                    </Grid>
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
    </StackPanel>
</Window>
//MainWindow.idl
namespace MiniEditor
{
    struct OpDatWRC
    {
        Int32 opnum;
        String name;
        Int32 op;
    };

    [default_interface]
    runtimeclass MainWindow : Microsoft.UI.Xaml.Window
    {
        MainWindow();
        void AddOp(String name, Int32 op);
        Windows.Foundation.Collections.IVectorView<OpDatWRC> GetOps();
    }
}
//MainWindow.xaml.h
#pragma once

#include "MainWindow.g.h"

namespace winrt::MiniEditor::implementation
{
    struct MainWindow : MainWindowT<MainWindow>
    {
        MainWindow();

        void AddOp(hstring const& name, int32_t op);
        winrt::Windows::Foundation::Collections::IVectorView<winrt::MiniEditor::OpDatWRC> GetOps();
    private:
        winrt::Windows::Foundation::Collections::IObservableVector<winrt::MiniEditor::OpDatWRC> ops{ winrt::single_threaded_observable_vector<winrt::MiniEditor::OpDatWRC>() };

    };
}

namespace winrt::MiniEditor::factory_implementation
{
    struct MainWindow : MainWindowT<MainWindow, implementation::MainWindow>
    {
    };
}
//MainWindow.xaml.cpp
#include "pch.h"
#include "MainWindow.xaml.h"
#if __has_include("MainWindow.g.cpp")
#include "MainWindow.g.cpp"
#endif

using namespace winrt;
using namespace Microsoft::UI::Xaml;

namespace winrt::MiniEditor::implementation
{
    MainWindow::MainWindow()
    {
        InitializeComponent();
        AddOp(L"One", 0);
        AddOp(L"Two", 1);
        AddOp(L"Three", 2);

        MEList().ItemsSource(ops);
    }
    void MainWindow::AddOp(hstring const& name, int32_t op)
    {
        // Create an instance of OpDatWRC
        winrt::MiniEditor::OpDatWRC newItem;
        newItem.opnum = ops.Size();
        newItem.name = name;
        newItem.op = op;

        // Add the new item to the IObservableVector
        ops.Append(newItem);
    }
    winrt::Windows::Foundation::Collections::IVectorView<winrt::MiniEditor::OpDatWRC> MainWindow::GetOps()
    {
        return ops.GetView();
    }
}

As shown, the binding to the ComboBox.SelectedIndex is "OneTime" and everything compiles and runs. However, when I switch to the desired "TwoWay" binding, I get the following compiler error:

C2064 term does not evaluate to a function taking 1 arguments

The error points to a line in MainWindow.xaml.g.hpp. Here's the code:

            case 8: // MainWindow.xaml line 38
                {
                    auto targetElement = target.as<::winrt::Microsoft::UI::Xaml::Controls::ComboBox>();
                    obj8 = targetElement;
                    obj8.RegisterPropertyChangedCallback(::winrt::Microsoft::UI::Xaml::Controls::Primitives::Selector::SelectedIndexProperty(),
                        [weakThis{ this->weak_from_this() }, this] (DependencyObject const& sender, DependencyProperty const& prop)
                        {
                            if (auto strongThis{ weakThis.lock() })
                            {
                                if (IsInitialized())
                                {
                                    // Update Two Way binding
                                    GetDataRoot().op(obj8.SelectedIndex()); //<= error is here
                                }
                            }
                        });
                }
                break;

What do I need to add to my code to fix this error? Do I need to explicitly define a setter for op?


Solution

  • Thanks to @SimonMourier, I have working code. Here's modified portion of the .idl:

        runtimeclass OpDatWRC : Microsoft.UI.Xaml.Data.INotifyPropertyChanged
        {
            OpDatWRC();
            Int32 opnum;
            String name;
            Int32 op;
        };
    

    Same for the .h:

    // OpDatWRC.h
    #pragma once
    #include "OpDatWRC.g.h"
    
    namespace winrt::MiniEditor::implementation
    {
        struct OpDatWRC : OpDatWRCT<OpDatWRC>
        {
            OpDatWRC() : m_opnum(0), m_name(L"BLANK"), m_op(0) {}
    
            int32_t opnum() { return m_opnum; }
            void opnum(int32_t value);
            hstring name() { return m_name; }
            void name(hstring const& value);
            int32_t op() { return m_op; }
            void op(int32_t value);
            winrt::event_token PropertyChanged(winrt::Microsoft::UI::Xaml::Data::PropertyChangedEventHandler const& handler);
            void PropertyChanged(winrt::event_token const& token) noexcept;
        private:
            int m_opnum;
            hstring m_name;
            int m_op;
            winrt::event<Microsoft::UI::Xaml::Data::PropertyChangedEventHandler> m_propertyChanged;
        };
    }
    namespace winrt::MiniEditor::factory_implementation
    {
        struct OpDatWRC : OpDatWRCT<OpDatWRC, implementation::OpDatWRC>
        {
        };
    }
    

    and .cpp:

    // OpDatWRC.cpp
    #include "pch.h"
    #include "OpDatWRC.h"
    #include "OpDatWRC.g.cpp"
    
    namespace winrt::MiniEditor::implementation
    {
        void OpDatWRC::opnum(int32_t value)
        {
            if (m_opnum != value)
            {
                m_opnum = value;
                m_propertyChanged(*this, Microsoft::UI::Xaml::Data::PropertyChangedEventArgs{ L"Opnum" });
            }
        }
        void OpDatWRC::name(hstring const& value)
        {
            if (m_name != value)
            {
                m_name = value;
                m_propertyChanged(*this, Microsoft::UI::Xaml::Data::PropertyChangedEventArgs{ L"Name" });
            }
        }
        void OpDatWRC::op(int32_t value)
        {
            if (m_op != value)
            {
                m_op = value;
                m_propertyChanged(*this, Microsoft::UI::Xaml::Data::PropertyChangedEventArgs{ L"Op" });
            }
        }
        winrt::event_token OpDatWRC::PropertyChanged(winrt::Microsoft::UI::Xaml::Data::PropertyChangedEventHandler const& handler)
        {
            return m_propertyChanged.add(handler);
        }
        void OpDatWRC::PropertyChanged(winrt::event_token const& token) noexcept
        {
            m_propertyChanged.remove(token);
        }
    }
    

    Finally, here's the updated XAML:

    <?xml version="1.0" encoding="utf-8"?>
    <Window
        x:Class="MiniEditor.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="using:MiniEditor"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        mc:Ignorable="d"
        Title="MiniEditor">
    
        <StackPanel Orientation="Vertical" HorizontalAlignment="Left" VerticalAlignment="Top">
            <ListView x:Name="MEList" Background="LightGoldenrodYellow" CanReorderItems="True" Height="600">
                <ListView.HeaderTemplate>
                    <DataTemplate>
                        <Grid>
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition Width="30"/>
                                <ColumnDefinition Width="80"/>
                                <ColumnDefinition Width="80"/>
                            </Grid.ColumnDefinitions>
                            <TextBlock Grid.Column="0" Text="#" HorizontalAlignment="Center"/>
                            <TextBlock Grid.Column="1" Text="Name" HorizontalAlignment="Center"/>
                            <TextBlock Grid.Column="2" Text="Op" HorizontalAlignment="Center"/>
                        </Grid>
                    </DataTemplate>
                </ListView.HeaderTemplate>
                <ListView.ItemTemplate>
                    <DataTemplate x:DataType="local:OpDatWRC">
                        <Grid x:Name="MEGrid">
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition Width="30"/>
                                <ColumnDefinition Width="80"/>
                                <ColumnDefinition Width="80"/>
                            </Grid.ColumnDefinitions>
                            <TextBlock Grid.Column="0" Text="{x:Bind Path=opnum}" Width="30" VerticalAlignment="Center" HorizontalAlignment="Right"/>
                            <TextBox Grid.Column="1" Text="{x:Bind Path=name, Mode=TwoWay}"/>
                            <ComboBox Grid.Column="2"  Width="80" SelectedIndex="{x:Bind Path=op, Mode=TwoWay}">
                            <!--<ComboBox Grid.Column="2"  Width="80" SelectedIndex="{x:Bind Path=op, Mode=OneTime}">-->
                                <x:String>&lt;</x:String>
                                <x:String>&gt;</x:String>
                                <x:String>=</x:String>
                                <x:String>|&lt;|</x:String>
                                <x:String>|&gt;|</x:String>
                                <x:String>|=|</x:String>
                            </ComboBox>
                        </Grid>
                    </DataTemplate>
                </ListView.ItemTemplate>
            </ListView>
        </StackPanel>
    </Window>
    

    The entire solution is in https://github.com/dr-eck/MiniEditor As a bonus, working code for the WinUI 3 version of the Bookstore example is added. The main trick for that code was changing Windows.UI.Xaml.Data.INotifyPropertyChanged to Microsoft.UI.Xaml.Data.INotifyPropertyChanged.