UWP/WinRT: How to use VisualState Triggers to chan

2019-07-23 17:19发布

问题:

In my UWP app, I have a series of AppBarButtons separated by AppBarSeparators. When the window size drops below a certain amount, I want to hide the AppBarSeparators to save space.

I tried something like this, but it didn't work:

<VisualState.Setters>
   <Setter Target="AppBarSeparator" Value="Collapsed"/>
</VisualState.Setters>

I understand it's not possible to give each of the AppBarSeparators labels so I can target them directly, because they are dynamically generated as part of a binding.

So how can I hide all AppBarSeparators when my window shrinks below a certain size?

Edit: Here is a stripped down version of my XAML to show how the AppBarSeparators are being generated:

<Pivot x:Name="docPivot"
       ItemsSource="{Binding}">

    <Pivot.ItemTemplate>
        <DataTemplate>
            <Grid>
                <Grid.RowDefinitions>
                    <RowDefinition Height="auto" />
                    <RowDefinition Height="auto" />
                </Grid.RowDefinitions>

                <StackPanel Orientation="Horizontal"
                            Grid.Row="0">
                    <AppBarButton/>
                    <AppBarSeparator/>
                    <AppBarButton/>
                    <AppBarButton/>
                    <AppBarSeparator/>
                </StackPanel>

                <StackPanel Orientation="Horizontal"
                            Grid.Row="1">
                </StackPanel>
            </Grid>
        </DataTemplate>
    </Pivot.ItemTemplate>

</Pivot>

回答1:

As we've discussed in the comment, your AppBarSeparators are generated in the Pivot's DataTemplate, when controls are placed inside the DateTemplate, they becomes the visual structure of your data objects, but VisualState targets the controls, so can it not work here.

You can use DataBinding with Converter to do this, and if the size of the window is changeable during the run-time, you may also need complete your data source class with INotifyPropertyChanged Interface.

For example here:

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

<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
    <Pivot x:Name="docPivot" ItemsSource="{x:Bind pivotlist}" SizeChanged="docPivot_SizeChanged">
        <Pivot.ItemTemplate>
            <DataTemplate>
                <Grid>
                    <Grid.RowDefinitions>
                        <RowDefinition Height="auto" />
                        <RowDefinition Height="auto" />
                    </Grid.RowDefinitions>
                    <StackPanel Orientation="Horizontal" Grid.Row="0">
                        <AppBarButton Icon="Accept" Label="Accept" />
                        <AppBarSeparator Visibility="{Binding WindowWidth, Converter={StaticResource cvt}}" />
                        <AppBarButton Icon="Cancel" Label="Cancel" />
                        <AppBarButton Icon="Add" Label="Add" />
                        <AppBarSeparator Visibility="{Binding WindowWidth, Converter={StaticResource cvt}}" />
                    </StackPanel>
                    <StackPanel Orientation="Horizontal"
                        Grid.Row="1">
                    </StackPanel>
                </Grid>
            </DataTemplate>
        </Pivot.ItemTemplate>
    </Pivot>
</Grid>

Code behind, I used FrameworkElement.SizeChanged event to get the window's width during the run-time, if you just want to make the layout fit for mobile or PC at the first time the layout loading is, then this event is not needed, neither is the INotifyPropertyChanged:

private ObservableCollection<MyPivotItem> pivotlist = new ObservableCollection<MyPivotItem>();

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

protected override void OnNavigatedTo(NavigationEventArgs e)
{
    pivotlist.Clear();
    pivotlist.Add(new MyPivotItem { });
    pivotlist.Add(new MyPivotItem { });
    pivotlist.Add(new MyPivotItem { });
    pivotlist.Add(new MyPivotItem { });
}

private void docPivot_SizeChanged(object sender, SizeChangedEventArgs e)
{
    foreach (var item in docPivot.Items)
    {
        var pivotitem = item as MyPivotItem;
        pivotitem.WindowWidth = Window.Current.Bounds.Width;
    }
}

The MyPivotItem class is like this:

public class MyPivotItem : INotifyPropertyChanged
{
    public MyPivotItem()
    {
        _windowwidth = Window.Current.Bounds.Width;
    }

    private double _windowwidth;

    public double WindowWidth
    {
        get { return _windowwidth; }
        set
        {
            if (value != _windowwidth)
            {
                _windowwidth = value;
                OnPropertyChanged();
            }
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;

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

And the BoolVisibleConverter is quite simple here:

public class BoolVisibleConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, string language)
    {
        double? width = (double?)value;
        if (width <= 700)
            return Visibility.Collapsed;
        return Visibility.Visible;
    }

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


回答2:

You can define a dependency property in your page:

sealed partial class Page1: Page {
     public static readonly DependencyProperty SeparatorVisibilityProperty = 
                            DependencyProperty.RegisterAttached("SeparatorVisibility",
                                           typeof(Visibility),
                                           typeof(Page1),
                                           new PropertyMetadata(Visibility.Visible)
                            );
    public bool SeparatorVisibility {
         get {
            return (Visibility)this.GetValue(SeparatorVisibilityProperty);
         }
         set {
             this.SetValue(SeparatorVisibilityProperty , value);
         }
    }
    ..
    ..

Then bind Visibility property of the AppBarSeparators to this property:

<Page ...
      ...
      x:Name="page">
    .. 
    ..
    <AppBarSeparator Visibility="{Binding ElementName=page, Path=SeparatorVisibility}"/>
    ..

Then change the SeparatorVisibility property of the page in the visual state:

<VisualState.Setters>
   <Setter Target="page.SeparatorVisibility" Value="Collapsed"/>
</VisualState.Setters>

The visual state changes the page property, it will change visibility of AppBarSeparators since their Visibility property are bound to the SeparatorVisibility property of the page. Not sure if it is the best solution, it's just what comes to my mind now.