Search code examples
comboboxc++-winrt

C++/WinRT: Setting combobox value DisplayMemberPath in XAML


I want to use a struct as item type of a combobox based on the following code:

MyUserControl.idl:

namespace my_app
{
    struct Info {

        String Id;
        String DisplayName;
    };

    [default_interface]
    runtimeclass MyUserControl : Microsoft.UI.Xaml.Controls.UserControl
    {
        MyUserControl();
    }
}  

MyUserControl.xaml:

...
<ComboBox x:Name="cbInfo" DisplayMemberPath="DisplayName"/>
...

MyUserControl.xaml.cpp:

void MyUserControl::SetInfo() 
{
    ...
    Info firstInfo = Info();
    firstInfo.Id = L"First identifier";
    firstInfo.DisplayName = L"First display name";
    cbInfo().Items().Append(winrt::box_value(firstInfo) );

    Info secondInfo = Info();
    secondInfo.Id = L"Second identifier";
    secondInfo.DisplayName = L"Second display name";
    cbInfo().Items().Append(winrt::box_value(secondInfo) );
    ...
}
       

The code compiles and runs, but the items in the combo box are displayed with an empty string. If I omit the attribute DisplayMemberPath in the XAML file, than for each item the following string is displayed:

Windows.Foundation.IReference`<my_app.Info>

How do I make the DisplayName value of the Info struct appear?

Update:

To avoid possible problems caused by boxing, I implemented the item type as a Windows Runtime Component for testing purposes:

InfoItem.idl:

namespace my_app
{
    [default_interface]
    runtimeclass InfoItem 
    {
        InfoItem();
        String Id;
        String DisplayName;
    }
}

InfoItem.h:

#pragma once

namespace winrt::my_app::implementation
{
    struct InfoItem : InfoItem T<InfoItem>
    {
        InfoItem() = default;

        hstring Id();
        void Id(hstring const& value);
        hstring DisplayName();
        void DisplayName(hstring const& value);

    private:
        hstring mId;
        hstring mDisplayName;
    };
}

namespace winrt::my_app::factory_implementation
{
    struct InfoItem : InfoItem T<InfoItem, implementation::InfoItem>
    {
    };
}

InfoItem.cpp

#include "pch.h"
#if __has_include("InfoItem.g.cpp")
#include "InfoItem.g.cpp"
#endif

namespace winrt::my_app::implementation
{
    hstring InfoItem::Id()
    {
        return mId;
    }
    void InfoItem::Id(hstring const& value)
    {
        mId = value;
    }
    hstring InfoItem::DisplayName()
    {
        return mDisplayName;
    }
    void InfoItem::DisplayName(hstring const& value)
    {
        mDisplayName = value;
    }
}

MyUserControl.xaml.cpp (with InfoItem):

void MyUserControl::SetInfo() 
{
    ...
    winrt::my_app::InfoItem firstItem = ::winrt::make<InfoItem>();
    firstItem.Id(L"First identifier");
    firstItem.DisplayName(L"First display name");
    cbInfo().Items().Append(firstItem);

    winrt::my_app::InfoItem secondItem = ::winrt::make<InfoItem>();
    secondItem.Id(L"Second identifier");
    secondItem.DisplayName(L"Second display name");
    cbInfo().Items().Append(secondItem);
    ...
}

This also does not work. The only difference is the displayed string if I omit the attribute DisplayMemberPath in the XAML file:

my_app.InfoItem

Does anyone have any ideas on how to get this to work?

Update 2:

According to the hints of Nico Zhu and the documentation "XAML items controls; bind to a C++/WinRT collection" [https://learn.microsoft.com/en-us/windows/uwp/cpp-and-winrt-apis/binding-collection] I have set the ItemsSource of the combobox to an IObservableVector. Furthermore I added the bindable attribute:

InfoItem.idl (with bindable attribute):

namespace my_app
{
    [bindable]
    [default_interface]
    runtimeclass InfoItem 
    {
        InfoItem();
        String Id;
        String DisplayName;
    }
}

MyUserControl.idl (with IObservableVector):

import "InfoItem.idl"
namespace my_app
{
    [default_interface]
    runtimeclass MyUserControl : Microsoft.UI.Xaml.Controls.UserControl
    {
        MyUserControl();
        IObservableVector<InfoItem> InfoList{ get; };
    }
}  

MyUserControl.xaml.h (with IObservableVector):

...
public:
     IObservableVector<winrt::my_app::InfoItem> InfoList();
private:
     IObservableVector<winrt::my_app::InfoItem> mInfoList;
...

MyUserControl.xaml.cpp (woth InfoItem and IObservableVector):

MyUserControl::MyUserControl()
{
    InitializeComponent();

    mInfoList = winrt::single_threaded_observable_vector<winrt::my_app::InfoItem>();
}

IObservableVector<winrt::my_app::InfoItem> MyUserControl::InfoList()
{
    return mInfoList;
}

void MyUserControl::SetInfo() 
{
    ...
    winrt::my_app::InfoItem firstItem = ::winrt::make<InfoItem>();
    firstItem.Id(L"First identifier");
    firstItem.DisplayName(L"First display name");
    mInfoList.Append(firstItem);

    winrt::my_app::InfoItem secondItem = ::winrt::make<InfoItem>();
    secondItem.Id(L"Second identifier");
    secondItem.DisplayName(L"Second display name");
    mInfoList.Append(secondItem);
    ...
}

MyUserControl.xaml (with IObservableVector):

...
<ComboBox x:Name="cbInfo" ItemsSource="{x:Bind InfoList}" DisplayMemberPath="DisplayName"/>
...

The result is still the same: : I see two items. When I add a handler for SelectionChanged, the items provided by the SelectionChangedEventArgs are correct, i.e. the list works. However, they are displayed empty, i.e. 'DisplayName' is not shown.

Any ideas?


Solution

  • Due to a hint from Nico Zhu and the post "How to populate the ComboBox by binding the ItemsSource property in desktop c++/winrt?" [https://github.com/microsoft/microsoft-ui-xaml/issues/4334] I finally found the solution (see below) . The properties of the combobox item (in the example "InfoItem") must be implemented as dependency properties. The attribute 'bindable' as well as the binding of the ItemsSource to a collection are not crucial in the end. Why and that the properties have to be implemented as dependency properties is currently not documented to my knowledge. Such important hints would make the work much easier. The WinUI 3 documentation is still quite poor. Hopefully this will change soon, otherwise WinUI 3 will never catch on. The implementation effort is also incomprehensibly high. While in C#/WPF the declaration of a simple helper structure for the display of the items in the combo box is sufficient, in C++/WinRT you have to create an elaborate DependencyObject. There is still a lot of room for improvement.
    Here is the working implementation (as of Windows App SDK 1.1):

    InfoItem.idl:

    namespace my_app
    {
        [default_interface]
        runtimeclass InfoItem  : Microsoft.UI.Xaml.DependencyObject
        {
            InfoItem();
            
            static Microsoft.UI.Xaml.DependencyProperty IdProperty{ get; };
            String Id;
    
            static Microsoft.UI.Xaml.DependencyProperty DisplayNameProperty{ get; };
            String DisplayName;
        }
    }
    

    InfoItem.h:

    #pragma once
    
    namespace winrt::my_app::implementation
    {
        struct InfoItem : InfoItem T<InfoItem>
        {
            static void InitClass();
    
            InfoItem() = default;
    
            static Microsoft::UI::Xaml::DependencyProperty IdProperty();
            hstring Id();
            void Id(hstring const& value);
    
            static Microsoft::UI::Xaml::DependencyProperty DisplayNameProperty();
            hstring DisplayName();
            void DisplayName(hstring const& value);
    
        private:
            static Microsoft::UI::Xaml::DependencyProperty mIdProperty;
            static Microsoft::UI::Xaml::DependencyProperty mDisplayNameProperty;
        };
    }
    
    namespace winrt::my_app::factory_implementation
    {
        struct InfoItem : InfoItem T<InfoItem, implementation::InfoItem>
        {
        };
    }
    

    InfoItem.cpp

    #include "pch.h"
    #if __has_include("InfoItem.g.cpp")
    #include "InfoItem.g.cpp"
    #endif
    
    namespace winrt::my_app::implementation
    {
        //static 
        Microsoft::UI::Xaml::DependencyProperty InfoItem::mIdProperty = nullptr;
        //static 
        Microsoft::UI::Xaml::DependencyProperty InfoItem::mDisplayNameProperty = nullptr;
    
        //static 
        void InfoItem::InitClass()
        {
            mIdProperty =
                Microsoft::UI::Xaml::DependencyProperty::Register(
                    L"Id",
                    winrt::xaml_typename<winrt::hstring>(),
                    winrt::xaml_typename<winrt::my_app::InfoItem>(),
                    Microsoft::UI::Xaml::PropertyMetadata{ nullptr }
            );
    
            mDisplayNameProperty =
                Microsoft::UI::Xaml::DependencyProperty::Register(
                    L"DisplayName",
                    winrt::xaml_typename<winrt::hstring>(),
                    winrt::xaml_typename<winrt::my_app::InfoItem>(),
                    Microsoft::UI::Xaml::PropertyMetadata{ nullptr }
            );
        }
        
        Microsoft::UI::Xaml::DependencyProperty InfoItem::IdProperty()
        {
            return mIdProperty;
        }
    
        hstring InfoItem::Id()
        {
            return winrt::unbox_value<winrt::hstring>(GetValue(mIdProperty));
        }
    
        void InfoItem::Id(hstring const& value)
        {
            SetValue(mIdProperty, winrt::box_value(value));
        }
    
        Microsoft::UI::Xaml::DependencyProperty InfoItem::DisplayNameProperty()
        {
            return mDisplayNameProperty;
        }
    
        hstring InfoItem::DisplayName()
        {
            return winrt::unbox_value<winrt::hstring>(GetValue(mDisplayNameProperty));
        }
    
        void InfoItem::DisplayName(hstring const& value)
        {
            SetValue(mDisplayNameProperty, winrt::box_value(value));
        }
    }
    

    Important!

    1. When searching for the implementation of Dependency Properties, you usually end up with this article at the moment: "Custom dependency properties" [https://learn.microsoft.com/en-us/windows/uwp/xaml-platform/custom-dependency-properties]. Please do not use the namespace "Windows::UI::Xaml::DependencyProperty" mentioned there but "Microsoft::UI::Xaml::DependencyProperty". The former compiles but leads to the following exception at runtime:

      WinRT originate error - 0x8001010E : 'The application called an interface that was marshalled for a different thread.'

    2. The registration of the dependency properties should be done in a static method to be able to control the time of registration. Otherwise, the registration may be called before the bootstrapper component has correctly initialized the Windows App SDK APIs. This will lead to an exception, e.g.

      WinRT originate error - 0x80040154 : 'Class not registered'

    MyUserControl.idl:

    namespace my_app
    {
        [default_interface]
        runtimeclass MyUserControl : Microsoft.UI.Xaml.Controls.UserControl
        {
            MyUserControl();
        }
    }  
    

    MyUserControl.xaml:

    ...
    <ComboBox x:Name="cbInfo" DisplayMemberPath="DisplayName"/>
    ...
    

    MyUserControl.xaml.cpp:

    void MyUserControl::SetInfo() 
    {
        ...
        winrt::my_app::InfoItem firstItem = ::winrt::make<InfoItem>();
        firstItem.Id(L"First identifier");
        firstItem.DisplayName(L"First display name");
        cbInfo().Items().Append(firstItem);
    
        winrt::my_app::InfoItem secondItem = ::winrt::make<InfoItem>();
        secondItem.Id(L"Second identifier");
        secondItem.DisplayName(L"Second display name");
        cbInfo().Items().Append(secondItem);
        ...
    }