Search code examples
c#xamlwinui-3winui

WinUI 3 weird problem with binding to a combobox, selectedItem always empty


I'm creating an app using WinUI 3. I have a weird binding problem. I'm doing the same thing a few times, that's why I can't seem to figure out why it is not working in this specific case.

This is the piece of code that's causing a problem:

<ComboBox Header="Base view"  
          HorizontalAlignment="Stretch"
           IsEnabled="{x:Bind ViewModel.SelectedViewConfiguration.BaseView, Converter={StaticResource IsNotNullConverter}, Mode=OneWay}"
           ItemsSource="{x:Bind ViewModel.OtherViewConfigurations, Mode=OneWay}" 
           SelectedItem="{x:Bind ViewModel.SelectedViewConfiguration.BaseView,  Mode=OneWay}">

    <ComboBox.ItemTemplate>
        <DataTemplate x:DataType="viewModels:ViewConfigurationViewModel">
            <TextBlock>
                View <Run Text="{x:Bind Number}" />
            </TextBlock>
        </DataTemplate>
    </ComboBox.ItemTemplate>
</ComboBox>

This part of the application allows the user to customize the position and options of views on a sheet. The 'views' are buttons on a canvas. I've attached a screenshot. On the button click I want to view some properties on the right, which works fine for all apart from the 'base view' property.

app views

The mode on the SelectedItem should be TwoWay. I've set it to OneWay now because I noticed that after clicking the button and executing the command the BaseView property is set to null.

Even when setting it to one way, the property is not displayed. I do however see the options when I press the dropdown.

view dropdown

My BaseView property in my viewmodel is an ObservableProperty. At the moment I'm setting the model property using the OnBaseViewChanged event.

[ObservableProperty]
private ViewConfigurationViewModel? baseView;

partial void OnBaseViewChanged(ViewConfigurationViewModel? value)
{
   ViewConfiguration.BaseView = value?.ViewConfiguration;        
}

This is my button clicked command handler

[RelayCommand]
private void ViewConfigurationClicked(ViewConfigurationViewModel item)
{
    SelectedViewConfiguration = item;
}

When I look in the Visual Tree I can see that the SelectedItem is 0. With the other comboboxes the value is loaded as expected.

Anyone has any idea what could be causing this? I can provide more code if necessary.

UPDATE - more context, I've removed some unrelated properties.

The page viewmodel:

public partial class DrawingsViewModel : ObservableRecipient { private readonly IDrawingService drawingService;

public DrawingsViewModel(IDrawingService drawingService)
{
    this.drawingService = drawingService;

    drawingConfigurations = new ObservableCollection<DrawingConfiguration>(drawingService.GetDrawingConfigurations());

    SelectedDrawingConfiguration = drawingConfigurations.First();
}

[ObservableProperty]
[NotifyPropertyChangedFor(nameof(OtherViewConfigurations))]
private ViewConfigurationViewModel? selectedViewConfiguration;

[ObservableProperty] 
private ObservableCollection<ViewConfigurationViewModel>? viewConfigurations;
public List<ViewConfigurationViewModel>? OtherViewConfigurations => ViewConfigurations?.Where(x => x != SelectedViewConfiguration).ToList();

[ObservableProperty] 
private ObservableCollection<DrawingConfiguration> drawingConfigurations;

[ObservableProperty] 
private DrawingConfiguration selectedDrawingConfiguration;

partial void OnSelectedDrawingConfigurationChanged(DrawingConfiguration value)
{
    ViewConfigurations = GetViews(SelectedDrawingConfiguration);

    // reset selected view when switching 
    SelectedViewConfiguration = null;
}

public ObservableCollection<ViewConfigurationViewModel> GetViews(DrawingConfiguration config)
{
    var vms = new List<ViewConfigurationViewModel>(config.Views
        .Select(x => new ViewConfigurationViewModel(x, config.SheetSize)));

    // go through the created viewmodels and assign the base view(model)
    foreach (var vm in vms)
    {
        if (vm.ViewConfiguration.BaseView == null) { continue; }

        var otherVms = vms.Where(x => x != vm).ToList();

        vm.BaseView = otherVms.FirstOrDefault(x => x.ViewConfiguration.Number == vm.ViewConfiguration.BaseView.Number);
    }

    return new ObservableCollection<ViewConfigurationViewModel>(vms);
}

// TODO: I should find a way to use a Selector or Listbox as container for the canvasview
[RelayCommand]
private void ViewConfigurationClicked(ViewConfigurationViewModel item)
{
    // item.BaseView is not null here
    SelectedViewConfiguration = item;
}

}

The viewmodel I use to represent the 'view' - I have not made a separate view for this yet, because I need to control the position of the items on the canvas, but this is irrelevant now.

public partial class ViewConfigurationViewModel : ObservableObject
{
    public readonly ViewConfiguration ViewConfiguration;
    private readonly SheetSize sheetSize;

    public int Number => ViewConfiguration.Number;

    public ViewConfigurationViewModel(ViewConfiguration viewConfiguration, SheetSize sheetSize)
    {
        this.ViewConfiguration = viewConfiguration;
    }
     
    public ViewType ViewType
    {
        get => ViewConfiguration.ViewType;
        set =>
            SetProperty(ViewConfiguration.ViewType, value,
                ViewConfiguration, (u, n) => u.ViewType = n);
    }

    [ObservableProperty]
    private ViewConfigurationViewModel? baseView;

    partial void OnBaseViewChanged(ViewConfigurationViewModel? value)
    {
        if (value == null)
        {
            return;
        }

        if (ViewConfiguration.BaseView != value?.ViewConfiguration) 
            ViewConfiguration.BaseView = value?.ViewConfiguration;
    }

}

This is the XAML, again I've removed some stuff:

            <!--left-->
            <controls1:CanvasView Height="600" Width="900" HorizontalAlignment="Left" 
                                      ItemsSource="{x:Bind ViewModel.ViewConfigurations, Mode=OneWay}">

                <controls1:CanvasView.ContextFlyout>
                    <MenuFlyout>
                        <MenuFlyoutItem Text="Add view" Command="{x:Bind ViewModel.AddViewCommand}" />
                    </MenuFlyout>
                </controls1:CanvasView.ContextFlyout>

                <controls1:CanvasView.ItemTemplate>
                    <DataTemplate x:DataType="viewModels:ViewConfigurationViewModel">

                        <!-- view config view -->
                        <Button Canvas.Left="{x:Bind Left, Mode=TwoWay}"
                                Canvas.Top="{x:Bind Top, Mode=TwoWay}"
                                Background ="{ThemeResource  CardBackgroundFillColorSecondaryBrush}"
                                BorderBrush ="{ThemeResource CardStrokeColorDefaultBrush}"
                                RequestedTheme="Light"
                                Padding="2"
                                BorderThickness="1"
                                Width="180"
                                Height="110"
                                CornerRadius="4"
                                ManipulationMode="TranslateX,TranslateY"
                                Command="{Binding ElementName=drawingPage, Path=ViewModel.ViewConfigurationClickedCommand}"
                                CommandParameter="{x:Bind}">

                            <Button.RenderTransform>
                                <TranslateTransform X="-90" Y="-55"></TranslateTransform>
                            </Button.RenderTransform>

                            <StackPanel>

                                <TextBlock FontWeight="SemiBold" FontSize="14" HorizontalTextAlignment="Center" Margin="0,0,0,10">
                                        View <Run Text="{x:Bind ViewConfiguration.Number}" />
                                </TextBlock>

                                <Grid Padding="3" ColumnSpacing="5">
                                    <Grid.ColumnDefinitions>
                                        <ColumnDefinition Width="*"/>
                                        <ColumnDefinition Width="*"></ColumnDefinition>
                                    </Grid.ColumnDefinitions>
                                    <NumberBox Header="X" FontSize="10" Text="{x:Bind X, Mode=OneWay}"></NumberBox>
                                    <NumberBox Grid.Column="1" Header="Y" FontSize="10" Text="{x:Bind Y, Mode=OneWay}"></NumberBox>
                                </Grid>

                            </StackPanel>
                        </Button>
                    </DataTemplate>
                </controls1:CanvasView.ItemTemplate>

            </controls1:CanvasView>

            <!--right-->
            <StackPanel Grid.Column="1" Spacing="10">

                <Expander HorizontalAlignment="Stretch" 
                          Margin="10,0,0,0"
                          IsExpanded="True"
                          Height="600"
                          HorizontalContentAlignment="Stretch"
                          VerticalContentAlignment="Stretch"
                          VerticalAlignment="Top"
                          Background="{ThemeResource CardBackgroundFillColorSecondaryBrush}">

                    <Expander.Header>
                        <StackPanel Orientation="Horizontal">
                            <TextBlock FontWeight="SemiBold">
                                View <Run Text="{x:Bind ViewModel.SelectedViewConfiguration.Number, Mode=OneWay, FallbackValue=''}"/>
                            </TextBlock>
                        </StackPanel>
                    </Expander.Header>

                    <StackPanel VerticalAlignment="Stretch">

                        <Grid Visibility="{x:Bind ViewModel.SelectedViewConfiguration, Converter={StaticResource IsNotNullToVisibilityConverter}, Mode=OneWay}">
                            <TextBlock Foreground="{ThemeResource SystemColorGrayTextColor}" HorizontalAlignment="Center"
                                       Text="No view selected">
                            </TextBlock>
                        </Grid>

                        <StackPanel Visibility="{x:Bind ViewModel.SelectedViewConfiguration, Converter={StaticResource IsNullToVisibilityConverter}, Mode=OneWay}" Spacing="5">

                            <ComboBox Header="View type" 
                                      HorizontalAlignment="Stretch"
                                      ItemsSource="{x:Bind ViewModel.ViewTypes}"
                                      SelectedItem="{x:Bind ViewModel.SelectedViewConfiguration.ViewType, Mode=TwoWay}">

                                <ComboBox.ItemTemplate>
                                    <DataTemplate x:DataType="config:ViewType">
                                        <TextBlock Text="{Binding Converter={StaticResource EnumToDisplayNameConverter}}"></TextBlock>
                                    </DataTemplate>
                                </ComboBox.ItemTemplate>
                            </ComboBox>

                            <ComboBox Header="Base view"  
                                      HorizontalAlignment="Stretch"
                                       IsEnabled="{x:Bind ViewModel.SelectedViewConfiguration.BaseView, Converter={StaticResource IsNotNullConverter}, Mode=OneWay}"
                                      SelectedItem="{x:Bind ViewModel.SelectedViewConfiguration.BaseView}">

                                <!-- TODO: what's wrong with this box - does not want to select anything --> 

                                <ComboBox.ItemTemplate>
                                    <DataTemplate x:DataType="viewModels:ViewConfigurationViewModel">
                                        <TextBlock>
                                            View <Run Text="{x:Bind Number}" />
                                        </TextBlock>
                                    </DataTemplate>
                                </ComboBox.ItemTemplate>
                            </ComboBox>

                        </StackPanel>
                    </StackPanel>
                </Expander>
            </StackPanel>
        </Grid>

I'm not sure about my approach creating the ViewConfigurationViewModels in the drawing class, but basically I have ViewConfiguration objects and they can have a BaseView which is anohter ViewConfiguration. On the page I select a DrawingConfigurations which triggers loading the views and other settings from there.

Update:

Test repo, everything is on the main page:

https://github.com/Basnederveen/BindingFailure


Solution

  • I do not understand exactly why, but the problem is fixed.

    The only thing I did is pull the property from the ViewConfigurationViewModel to the DrawingsViewModel class, and bind to that one instead:

    public ViewConfigurationViewModel? SelectedBaseViewConfiguration
    {
        get => SelectedViewConfiguration?.BaseView;
        set => SelectedViewConfiguration.BaseView = value;
    }
    

    And add this attribute to SelectedViewConfiguration.

    [NotifyPropertyChangedFor(nameof(SelectedBaseViewConfiguration))]
    

    Then it works fine. I guess because it is nested and the view is bound to only the mainview the property changed events do not work.