Search code examples
c#xamldata-bindingwinui-3winui

Using WinUI3 and XAML data binding, how do you get a field to update when the source contains an indexing operation and is a subfield?


I have a state class named PlayerState that contains many fields I want to bind via XAML, so I want a solution that scales well.

I have this code in a ViewModel that inherits from the BindableBase class provided by Microsoft so that binding gets triggered when SetProperty is called. I have verified that SetProperty is called with data that is correct.

private PlayerState _playerState;
public PlayerState PlayerState
{
    get { return _playerState; }
    set { SetProperty(ref _playerState, value); }
}

internal void OnStateChange(PlayerState playerState)
{
    PlayerState = playerState;
}

I have this binding that contains a context to the ViewModel described above, and have no clue how I am supposed to debug XAML. The error I get is that 'Active' property not found on 'App.Models.PlayerState'. All the fields in PlayerState are public and readonly.

<Image
    x:Name="ActiveImage"
    Source="{Binding PlayerState.Active.ImageFileNames[ImageSize.SMALL], Mode=OneWay, FallbackValue=/Assets/BlankCard2.png}"
    Margin="5"
    Grid.Row="0"
    Grid.Column="5" />

I set the context in the code behind file.

public PlayerPage()
{
    InitializeComponent();
    DataContext = ViewModel;
}

But when the PlayerState's Active field is changed and PlayerState updated, the binding does not update the image. Why? The path Active.ImageFileNames[ImageSize.SMALL] is correct, because it works when I bind them to a ListView.

I would use bind, but it does not support indexing, so what could be wrong here?


Solution

  • I'm assuming that ImageSize is an enum, something like this:

    public enum ImageSize
    {
        SMALL,
        MEDIUM,
        LARGE,
    }
    

    But IIRC, bindings won't work with Dictionary with enum as its key, in this case, Dictionary<ImageSize, string>.

    So, one workaround should be using Dictionary<string, string>.

    Another workaround might be creating a value converter:

    public class TestConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, string language)
        {
            if (value is IDictionary<ImageSize, string> dictionary &&
                parameter is string parameterString &&
                Enum.TryParse(parameterString, out ImageSize imageSize) is true)
            {
                if (dictionary.TryGetValue(imageSize, out var fileName))
                {
                    return fileName;
                }
            }
    
            throw new ArgumentException($"Failed to convert to file name. [value: {value} / parameter: {parameter}]");
        }
    
        public object ConvertBack(object value, Type targetType, object parameter, string language)
        {
            throw new NotImplementedException();
        }
    }
    

    and use it like this:

    <Image Source="{Binding PlayerState.Active.ImageFileNames, Mode=OneWay, Converter={StaticResource TestConverter}, ConverterParameter='SMALL'}" />
    

    UPDATE

    This is the rest of my test code for this answer:

    public class Active
    {
        public Dictionary<ImageSize, string> ImageFileNames { get; } = new()
        {
            { ImageSize.SMALL, "Assets/Small.png" },
            { ImageSize.MEDIUM, "Assets/Medium.png" },
            { ImageSize.LARGE, "Assets/Large.png" },
        };
    }
    
    public partial class PlayerState : ObservableObject
    {
        [ObservableProperty]
        private Active? active;
    }
    
    public partial class MainPageViewModel : ObservableObject
    {
        [ObservableProperty]
        private PlayerState playerState = new();
    
        [RelayCommand]
        private void Test()
        {
            PlayerState.Active = new Active();
        }
    }
    

    NOTE

    I'm using the CommunityToolkit.Mvvm NuGet package for the MVVM pattern and I recommend you give it a try. It's just amazing.