Cleve Cleve - 26 days ago 9
C# Question

How to update all items shown by ItemsControl using a condition?

In a UWP Project I have a UI which has an ItemsControl bound to a set of Team objects. There is a separate GameController object that has a CurrentTeam property which changes as the Game progresses. I want to be able to have a visual cue in the ItemTemplate for the Team that is the CurrentTeam. An example would be the Current Team's name gets underlined say. The Team objects do not have a reference to the GameController.

One way is to put a flag on each Team, say IsCurrentTeam and bind to that in the ItemTemplate. I don't particularly like this approach as it means when the CurrentTeam changes I've got to loop around all the Teams except the current one, to update their flags.

In WPF I think there might have been a solution using an ObjectDataProvider as it offers the ability to bind to a method, but since I'm in UWP this option is not available.

Does anyone know of a better approach to do this?

Many thanks in advance for any help.

Answer

Ok, I've prepared an example that shows how this achievable. To work around limitations in UWP it uses a few techniques such as 'data context anchoring' and attached properties.

Here's my support classes, I assume they're somewhat similar to yours:

public class GameControllerViewModel : INotifyPropertyChanged
{
    private Team _currentTeam;

    public event PropertyChangedEventHandler PropertyChanged;

    public GameControllerViewModel(IEnumerable<Team> teams)
    {
        Teams = teams;
    }

    private void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

    public Team CurrentTeam
    {
        get { return _currentTeam; }
        set
        {
            if (value != _currentTeam)
            {
                _currentTeam = value;

                OnPropertyChanged();
            }
        }
    }

    public IEnumerable<Team> Teams { get; private set; }
}

public class Team
{
    public string Name { get; set; }
}

And the code behind of the page:

public sealed partial class GamesPage : Page
{
    public GamesPage()
    {
        this.InitializeComponent();

        this.DataContext = new GameControllerViewModel(
            new[]
            {
                new Team { Name = "Team A" },
                new Team { Name = "Team B" },
                new Team { Name = "Team C" },
                new Team { Name = "Team D" }
            }
        );
    }
}

As you can see, the constructor of the page instantiates a GameControllerViewModel with four teams and sets it as the data context of the page.

The page XAML is as follows:

<Page
    x:Class="UniversalScratchApp.GamesPage" x:Name="View"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:UniversalScratchApp"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d">

    <Page.Resources>
        <local:BoolToFontWeightConverter x:Key="BoolToFontWeightConverter"/>
    </Page.Resources>

    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition />
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition />
        </Grid.RowDefinitions>
        <TextBlock Grid.Row="0" Grid.Column="0" Text="Current Team:" Margin="4" VerticalAlignment="Center"/>
        <ComboBox Grid.Row="0" Grid.Column="1" ItemsSource="{Binding Teams}" SelectedItem="{Binding CurrentTeam, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" HorizontalAlignment="Stretch" Margin="4">
            <ComboBox.ItemTemplate>
                <DataTemplate>
                    <TextBlock Text="{Binding Name}" />
                </DataTemplate>
            </ComboBox.ItemTemplate>
        </ComboBox>
        <ItemsControl Grid.Row="1" Grid.ColumnSpan="2" ItemsSource="{Binding Teams}">
            <ItemsControl.ItemTemplate>
                <DataTemplate>
                    <TextBlock Text="{Binding Name}" local:TeamProperties.CurrentTeam="{Binding ElementName=View, Path=DataContext.CurrentTeam}" local:TeamProperties.Team="{Binding}" FontWeight="{Binding Path=(local:TeamProperties.IsCurrentTeam), RelativeSource={RelativeSource Mode=Self}, Mode=OneWay, Converter={StaticResource BoolToFontWeightConverter}}"/>
                </DataTemplate>
            </ItemsControl.ItemTemplate>
        </ItemsControl>
    </Grid>
</Page>

In the DataTemplate of the ItemsControl you can see that I bind to a three custom attached properties; TeamProperties.CurrentTeam, TeamProperties.Team and TeamProperties.IsCurrentTeam. The attached properties are defined in the following class:

[Bindable]
public static class TeamProperties
{
    private static void TeamPropertiesChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
    {
        Team team = GetTeam(sender);
        Team currentTeam = GetCurrentTeam(sender);

        if (team != null && currentTeam != null)
        {
            SetIsCurrentTeam(sender, team.Equals(currentTeam));
        }
    }

    public static readonly DependencyProperty CurrentTeamProperty = DependencyProperty.RegisterAttached("CurrentTeam", typeof(Team), typeof(TeamProperties), new PropertyMetadata(null, TeamPropertiesChanged));

    public static Team GetCurrentTeam(DependencyObject obj)
    {
        return (Team)obj.GetValue(CurrentTeamProperty);
    }

    public static void SetCurrentTeam(DependencyObject obj, Team value)
    {
        obj.SetValue(CurrentTeamProperty, value);
    }

    public static readonly DependencyProperty TeamProperty = DependencyProperty.RegisterAttached("Team", typeof(Team), typeof(TeamProperties), new PropertyMetadata(null, TeamPropertiesChanged));

    public static Team GetTeam(DependencyObject obj)
    {
        return (Team)obj.GetValue(TeamProperty);
    }

    public static void SetTeam(DependencyObject obj, Team value)
    {
        obj.SetValue(TeamProperty, value);
    }

    public static readonly DependencyProperty IsCurrentTeamProperty = DependencyProperty.RegisterAttached("IsCurrentTeam", typeof(bool), typeof(TeamProperties), new PropertyMetadata(false));

    public static bool GetIsCurrentTeam(DependencyObject obj)
    {
        return (bool)obj.GetValue(IsCurrentTeamProperty);
    }

    public static void SetIsCurrentTeam(DependencyObject obj, bool value)
    {
        obj.SetValue(IsCurrentTeamProperty, value);
    }
}

To explain, the CurrentTeam and Team properties are set on the dependency object (the textblock) by the bindings. While the Team property can use the current datacontext, the CurrentTeam property must be bound to the 'outer' DataContext. It does this by specifying an x:Name="View" on the Page and using that to 'anchor' the datacontext so it can then be accessed by bindings using the ElementName=View part of the binding.

So, whenever either of these properties change, the IsCurrentTeam property is set on the same dependency object by the TeamPropertiesChanged callback. The IsCurrentTeam property then is bound to the FontWeight property (as it was easier than underlining) with the BoolToFontWeightConverter shown here:

public class BoolToFontWeightConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, string language)
    {
        if (value is bool)
        {
            return ((bool)value) ? FontWeights.ExtraBold : FontWeights.Normal;
        }
        else
        {
            return DependencyProperty.UnsetValue;
        }
    }

    public object ConvertBack(object value, Type targetType, object parameter, string language)
    {
        throw new NotSupportedException();
    }
}

Now, when a team is selected in the top combobox (a proxy for whatever mechanism you use to change teams) the appropriate team in the ItemsControl will be displayed in bold.

Works nicely for me. Hope it helps.