Michael Puckett II
Michael Puckett II

Reputation: 6749

How to dynamically change itemtemplate in UWP?

In WPF you can modify controls using Styles and Templates dynamically with Binding. I see how to do this in UWP directly in the control but I want to apply a template that will change itself based on the binding.

An example would be a button. I have a button that turns on and off a light in this project. The project is already created and running in WPF but needs to be converted to UWP. In the WPF version we have a LightStyle for the button, depending on what type of light it is, we change the Template to look and perform for that light. (For example: we can change the color of some lights, the dimness for some lights, and some lights just turn on and off; but we use the same LightStyle for them all. Very generic, dynamic, and extremely useful.)

How do you do this in UWP? I've searched a minute and figured I would stop here and check while I continue to dig. Keep in mind that this project is pure MVVM and no code behind is used. I don't mind a code behind explanation as long as it's not the only way.

Thanks in advance :)

Upvotes: 1

Views: 3513

Answers (2)

Romasz
Romasz

Reputation: 29792

Here is a sample I've made - XAML:

<StackPanel Background="{ThemeResource ApplicationPageBackgroundThemeBrush}" Orientation="Horizontal">
    <StackPanel.Resources>
        <local:MySelector x:Key="MySelector">
            <local:MySelector.GreenTemplate>
                <DataTemplate>
                    <TextBlock Text="{Binding Text}" Foreground="Green"/>
                </DataTemplate>
            </local:MySelector.GreenTemplate>
            <local:MySelector.RedTemplate>
                <DataTemplate>
                    <TextBlock Text="{Binding Text}" Foreground="Red"/>
                </DataTemplate>
            </local:MySelector.RedTemplate>
        </local:MySelector>
    </StackPanel.Resources>

    <ListView x:Name="ListOfItems" Width="100" ItemTemplateSelector="{StaticResource MySelector}"/>
    <StackPanel>
        <ToggleSwitch OnContent="GREEN" OffContent="RED" Margin="10" IsOn="{x:Bind IsSwitched, Mode=TwoWay}"/>
        <Button Content="Add item" Click="AddClick" Margin="10"/>
    </StackPanel>
</StackPanel>

and the code behind:

public sealed partial class MainPage : Page, INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    private void RaiseProperty(string name) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));

    private bool isSwitched = false;
    public bool IsSwitched
    {
        get { return isSwitched; }
        set { isSwitched = value; RaiseProperty(nameof(IsSwitched)); }
    }

    public MainPage() { this.InitializeComponent(); }

    private void AddClick(object sender, RoutedEventArgs e)
    {
        ListOfItems.Items.Add(new ItemClass { Type = isSwitched ? ItemType.Greed : ItemType.Red, Text = "NEW ITEM" });
    }
}

public enum ItemType { Red, Greed };

public class ItemClass
{
    public ItemType Type { get; set; }
    public string Text { get; set; }
}

public class MySelector : DataTemplateSelector
{
    protected override DataTemplate SelectTemplateCore(object item, DependencyObject container)
    {
        switch ((item as ItemClass).Type)
        {
            case ItemType.Greed:
                return GreenTemplate;
            case ItemType.Red:
            default:
                return RedTemplate;
        }
    }

    public DataTemplate GreenTemplate { get; set; }
    public DataTemplate RedTemplate { get; set; }
}

Generally you can choose various switches for your selector, it depends on your needs. In above example I'm switching the template basing on the item's property, here is a good example how to switch on item's type.

Upvotes: 2

Michael Puckett II
Michael Puckett II

Reputation: 6749

Here's the answer that I am using that works for my given situation. Basically you have to use a VisualStateTrigger and create the trigger manually via code. There are various triggers you can use and many built in but for this situation I had to, or at least I think I had to, write one manually.

Here's the trigger code.

public class StringComparisonTrigger : StateTriggerBase
{
    private const string NotEqual = "NotEqual";
    private const string Equal = "Equal";

    public string DataValue
    {
        get { return (string)GetValue(DataValueProperty); }
        set { SetValue(DataValueProperty, value); }
    }

    public static readonly DependencyProperty DataValueProperty =
        DependencyProperty.Register(nameof(DataValue), typeof(string), typeof(StringComparisonTrigger), new PropertyMetadata(Equal, (s, e) =>
        {
            var stringComparisonTrigger = s as StringComparisonTrigger;
            TriggerStateCheck(stringComparisonTrigger, stringComparisonTrigger.TriggerValue, (string)e.NewValue);
        }));


    public string TriggerValue
    {
        get { return (string)GetValue(TriggerValueProperty); }
        set { SetValue(TriggerValueProperty, value); }
    }

    public static readonly DependencyProperty TriggerValueProperty =
        DependencyProperty.Register(nameof(TriggerValue), typeof(string), typeof(StringComparisonTrigger), new PropertyMetadata(NotEqual, (s, e) =>
        {
            var stringComparisonTrigger = s as StringComparisonTrigger;
            TriggerStateCheck(stringComparisonTrigger, stringComparisonTrigger.DataValue, (string)e.NewValue);
        }));


    private static void TriggerStateCheck(StringComparisonTrigger elementTypeTrigger, string dataValue, string triggerValue)
        => elementTypeTrigger.SetActive(dataValue == triggerValue);
}

This, since inheriting from StateTriggerBase can be used in the VisualStateTriggers group as I will post below. What I didn't know is that any dependency property you write can be used in the XAML and there's no interfaces or anything in the trigger to make it work. The only line of code that fires the trigger is 'SetActive(bool value)' that you must call whenever you want the state to change. By making dependency properties and binding in the XAML you can fire the SetActive whenever the property is changed and therefore modify the visual state.

The DataTemplate is below.

<DataTemplate x:Key="LightsButtonTemplate">

    <UserControl>
        <StackPanel Name="panel">

            <VisualStateManager.VisualStateGroups>
                <VisualStateGroup>
                    <VisualState>
                        <VisualState.StateTriggers>
                            <DataTriggers:StringComparisonTrigger DataValue="{Binding Type}"
                                                                  TriggerValue="READ" />
                        </VisualState.StateTriggers>
                        <VisualState.Setters>
                            <Setter Target="panel.(UIElement.Background)"
                                    Value="Red" />
                        </VisualState.Setters>
                    </VisualState>
                </VisualStateGroup>
            </VisualStateManager.VisualStateGroups>

            <TextBlock Text="{Binding Type}" />
            <TextBlock Text="{Binding LightStateViewModel.On}" />

        </StackPanel>
    </UserControl>

</DataTemplate>

And finally using you can use the DataTemplate anywhere but I am using it in an ItemsControl that is bound to a list of LightViewModels.

    <ScrollViewer Grid.Row="1">
        <ItemsControl ItemsSource="{Binding LightViewModels}"
                      ItemTemplate="{StaticResource LightsButtonTemplate}" />
    </ScrollViewer>

Obviously this isn't the template design I want for the light buttons but this is all I've done to understand and now implement dynamic templates. Hopefully this helps someone else coming from WPF.

The custom trigger class deriving from StateTriggerBase can do and bind anyway you want it to and all you need to do is call SetActive(true) or SetActive(false) whenever you wish to update that trigger. When it's true the VisualState using that trigger will be active.

Upvotes: 0

Related Questions