xamlmauivisualstatemanager

How to use VisualStateManager in a ContentView in Xaml & Maui?


I use a specific Border control multiple times throughout my app. This Border control is displayed as part of a CollectionView. I used VisualStateManager to control the Border color when the Border and it's contained item was selected within the CollectionView. Here is where the Border control was defined:

       <CollectionView Margin="0,10,0,0" ItemsLayout="HorizontalList" HorizontalOptions="Center"
                        ItemsSource="{Binding Source={x:Static vm:WaxItVM.SnowSourceButtons}}"
                        SelectionMode="Single" SelectedItem="{Binding SelectedSnowSourceButton}">
            <CollectionView.ItemTemplate>
                <DataTemplate>
                    <Border StrokeShape="RoundRectangle 10" StrokeThickness="2" Margin="4,0,4,0">
                        <Image Source="{Binding IconFileName}" Margin="6,6,6,6" WidthRequest="35" HeightRequest="35" VerticalOptions="Center" HorizontalOptions="Center">
                            <Image.Behaviors>
                                <mct:IconTintColorBehavior TintColor="{AppThemeBinding Light={StaticResource Black},Dark={StaticResource White}}"/>
                            </Image.Behaviors>
                        </Image>
                    </Border>
                </DataTemplate>
            </CollectionView.ItemTemplate>
        </CollectionView>

Here is how I used VisualStateManager in the same ContentPage file:

    <ContentPage.Resources>
        <Style TargetType="Border">
            <Setter Property="VisualStateManager.VisualStateGroups">
                <VisualStateGroupList>
                    <VisualStateGroup x:Name="CommonStates">
                        <VisualState x:Name="Normal" />
                        <VisualState x:Name="Selected">
                            <VisualState.Setters>
                                <Setter Property="Stroke" Value="{StaticResource Primary}" />
                                <Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource White},Dark={StaticResource Black}}" />
                            </VisualState.Setters>
                        </VisualState>
                    </VisualStateGroup>
                </VisualStateGroupList>
            </Setter>
        </Style>
    </ContentPage.Resources>

That all worked beautifully. I then decided to extract the Border control into a separate Xaml file named SnowConditionInputButton.xaml containing a ContentView. Here is what that looked like...

<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:mct="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
             x:Class="FasterWaxer.CustomComponents.SnowConditionInputButton">
    
    <Border StrokeShape="RoundRectangle 10" StrokeThickness="2" Margin="4,0,4,0">
        <Image Source="{Binding IconFileName}" Margin="6,6,6,6" WidthRequest="35" HeightRequest="35" VerticalOptions="Center" HorizontalOptions="Center">
            <Image.Behaviors>
                <mct:IconTintColorBehavior TintColor="{AppThemeBinding Light={StaticResource Black},Dark={StaticResource White}}"/>
            </Image.Behaviors>
        </Image>
    </Border>
    
</ContentView>

I then changed my ContentPage to:

       <CollectionView Margin="0,10,0,0" ItemsLayout="HorizontalList" HorizontalOptions="Center"
                        ItemsSource="{Binding Source={x:Static vm:WaxItVM.SnowSourceButtons}}"
                        SelectionMode="Single" SelectedItem="{Binding SelectedSnowSourceButton}">
            <CollectionView.ItemTemplate>
                <DataTemplate>
                    <custom_components:SnowConditionInputButton/>
                </DataTemplate>
            </CollectionView.ItemTemplate>
        </CollectionView>

When I did that, the each CollectionView item was displayed correctly but the Border color no longer changed when I selected a CollectionView item. It seems that the ContentPage.Resources no longer applied to the Border control when I moved it to the separate ContentView. So, I tried to move VisualStateManager to the ContentView to explicitly control the Border color. When doing so, the Border still doesn't respond to being selected within the CollectionView:

    <Border StrokeShape="RoundRectangle 10" StrokeThickness="2" Margin="4,0,4,0">
        <VisualStateManager.VisualStateGroups>
            <VisualStateGroupList>
                <VisualStateGroup x:Name="CommonStates">
                    <VisualState x:Name="Normal" />
                    <VisualState x:Name="Selected">
                        <VisualState.Setters>
                            <Setter Property="Stroke" Value="{StaticResource Primary}" />
                            <Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource White},Dark={StaticResource Black}}" />
                        </VisualState.Setters>
                    </VisualState>
                </VisualStateGroup>
            </VisualStateGroupList>
        </VisualStateManager.VisualStateGroups>
        <Image Source="{Binding IconFileName}" Margin="6,6,6,6" WidthRequest="35" HeightRequest="35" VerticalOptions="Center" HorizontalOptions="Center">
            <Image.Behaviors>
                <mct:IconTintColorBehavior TintColor="{AppThemeBinding Light={StaticResource Black},Dark={StaticResource White}}"/>
            </Image.Behaviors>
        </Image>
    </Border>

How can I extract that Border control and the related VisualStateManager into a separate Xaml file for reuse? As a side note, I don't want that Border configuration to apply to all Borders throughout the app so I don't want to define it as a global Border Style that applies to all Borders. I want to selectively apply it to specific Borders or to all Borders on a page.

Thanks for your help.


Solution

  • You could make some changes to your code:

    First, in the ContentView, give a Name to the Border. Let's call it myBorder which will be referenced in our VisualStateManger.

    <ContentView  xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             ...>
    
        <Border  x:Name="myBorder"  StrokeShape="RoundRectangle 10" StrokeThickness="2" Margin="4,0,4,0" HeightRequest="50" >
        ...
        </Border>
    </ContentView>
    

    In ContentPage, make some changes to our VisualStateManager. As the VisualStateGroups is attached to Custom Control SnowConditionInputButtonm, we should set the TargetName and Property to Border like the following:

    <ContentPage.Resources>
        <Style  TargetType="custom_components:SnowConditionInputButton">
            <Setter Property="VisualStateManager.VisualStateGroups">
                <VisualStateGroupList>
                    <VisualStateGroup x:Name="CommonStates">
                        <VisualState x:Name="Normal" />
                        <VisualState x:Name="Selected">
                            <VisualState.Setters>
                                <Setter TargetName="myBorder" Property="Border.Stroke" Value="{StaticResource Primary}" />
                                <Setter TargetName="myBorder" Property="Border.BackgroundColor" Value="{AppThemeBinding Light={StaticResource White},Dark={StaticResource Black}}" />
                            </VisualState.Setters>
                        </VisualState>
                    </VisualStateGroup>
                </VisualStateGroupList>
            </Setter>
        </Style>
    </ContentPage.Resources>
    

    Then easily consume Custom Control in CollectionView:

    <CollectionView.ItemTemplate>
        <DataTemplate>
            <custom_components:SnowConditionInputButton/>    
        </DataTemplate>
    </CollectionView.ItemTemplate>
    

    If you don't want to use Style like the above code, you could easily define VisualStateManager in SnowConditionInputButton. It also works fine.

    <custom_components:SnowConditionInputButton HeightRequest="100">
        <VisualStateManager.VisualStateGroups>
            <VisualStateGroupList>
                <VisualStateGroup x:Name="CommonStates">
                    <VisualState x:Name="Normal" />
                    <VisualState x:Name="Selected">
                        <VisualState.Setters>
                            <Setter TargetName="myBorder" Property="Border.Stroke" Value="{StaticResource Primary}" />
                            <Setter TargetName="myBorder" Property="Border.BackgroundColor" Value="{AppThemeBinding Light={StaticResource White},Dark={StaticResource Black}}" />
                        </VisualState.Setters>
                    </VisualState>
                </VisualStateGroup>
            </VisualStateGroupList>
        </VisualStateManager.VisualStateGroups>
    </custom_components:SnowConditionInputButton>
    

    Hope it works for you.