0
votes

is it possible to bind an UWP CommandBar to something like a ObservableCollection or so?

What i want to achieve ist to bind my CommandBar of my NavigationView to an Object of a specific Page so that the AppBarButton change dynamicaly depending on the current Page

What i tryed:

MainPage.xaml

    <NavigationView.HeaderTemplate>
        <DataTemplate>
            <Grid>
                <CommandBar Grid.Column="1"
                        HorizontalAlignment="Right"
                        VerticalAlignment="Top"
                        DefaultLabelPosition="Right"
                        Background="{ThemeResource SystemControlBackgroundAltHighBrush}"  Content="{Binding Path=Content.AppBarButtonList, ElementName=rootFrame}">
                </CommandBar>
            </Grid>
        </DataTemplate>
    </NavigationView.HeaderTemplate>

SomePage.xaml.cs

    public ObservableCollection<AppBarButton> AppBarButtonList = new ObservableCollection<AppBarButton> {
        new AppBarButton { Icon = new SymbolIcon(Symbol.Accept), Label="Bla" },
        new AppBarButton{Icon=new SymbolIcon(Symbol.Add),Label="Add"}
    };

But the CommandBar shows nothing.

Thanks.

2

2 Answers

2
votes

My original solution was using the PrimaryCommands property to bind the commands, but it turns out this property is read-only.

My solution to the problem will be using behaviors.

First add a reference to Microsoft.Xaml.Behaviors.Uwp.Managed from NuGet.

Then add the following behavior to your project:

public class BindableCommandBarBehavior : Behavior<CommandBar>
{
    public ObservableCollection<AppBarButton> PrimaryCommands
    {
        get { return (ObservableCollection<AppBarButton>)GetValue(PrimaryCommandsProperty); }
        set { SetValue(PrimaryCommandsProperty, value); }
    }

    public static readonly DependencyProperty PrimaryCommandsProperty = DependencyProperty.Register(
        "PrimaryCommands", typeof(ObservableCollection<AppBarButton>), typeof(BindableCommandBarBehavior), new PropertyMetadata(default(ObservableCollection<AppBarButton>), UpdateCommands));

    private static void UpdateCommands(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs)
    {
        if (!(dependencyObject is BindableCommandBarBehavior behavior)) return;
        var oldList = dependencyPropertyChangedEventArgs.OldValue as ObservableCollection<AppBarButton>;
        if (dependencyPropertyChangedEventArgs.OldValue != null)
        {
            oldList.CollectionChanged -= behavior.PrimaryCommandsCollectionChanged;
        }

        var newList = dependencyPropertyChangedEventArgs.NewValue as ObservableCollection<AppBarButton>;
        if (dependencyPropertyChangedEventArgs.NewValue != null)
        {
            newList.CollectionChanged += behavior.PrimaryCommandsCollectionChanged;
        }
        behavior.UpdatePrimaryCommands();
    }


    private void PrimaryCommandsCollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
    {
        UpdatePrimaryCommands();
    }


    private void UpdatePrimaryCommands()
    {
        if (PrimaryCommands != null)
        {
            AssociatedObject.PrimaryCommands.Clear();
            foreach (var command in PrimaryCommands)
            {
                AssociatedObject.PrimaryCommands.Add(command);
            }
        }
    }

    protected override void OnDetaching()
    {
        base.OnDetaching();
        if (PrimaryCommands != null)
        {
            PrimaryCommands.CollectionChanged -= PrimaryCommandsCollectionChanged;
        }
    }
}

This behavior essentially creates a fake PrimaryCommands property that is bindable and also observes collection changed events. Whenever a change occurs, the commands are rebuilt.

Finally, the problem in your code is that your AppBarButtonList is just a field, not a property. Change it like this:

public ObservableCollection<AppBarButton> AppBarButtonList { get; } = new ObservableCollection<AppBarButton> {
    new AppBarButton { Icon = new SymbolIcon(Symbol.Accept), Label="Bla" },
    new AppBarButton{Icon=new SymbolIcon(Symbol.Add),Label="Add"}
};

Notice the {get ;} which was added before the assignment operator.

Now you can use the behavior in XAML like this:

<CommandBar>
    <interactivity:Interaction.Behaviors>
        <local:BindableCommandBarBehavior PrimaryCommands="{Binding Path=Content.AppBarButtonList, ElementName=rootFrame}" />
    </interactivity:Interaction.Behaviors>
</CommandBar>

This is by no means a perfect solution and could be improved upon to allow different collection types binding and more, but it should cover your scenario. An alternative solution would be to implement a custom version of command bar, with new additional dependency property directly on the type, but I used behavior to make it clearer for the user that this is an "added" functionality, not a built-in one.

0
votes

I found this answer very helpful. I did some more adjustments, like using a DataTemplateSelector to remove UI references like "AppBarButton" from the bindable data source.

public class BindableCommandBarBehavior : Behavior<CommandBar>
{
    public static readonly DependencyProperty PrimaryCommandsProperty = DependencyProperty.Register(
        "PrimaryCommands", typeof(object), typeof(BindableCommandBarBehavior),
        new PropertyMetadata(null, UpdateCommands));

    public static readonly DependencyProperty ItemTemplateSelectorProperty = DependencyProperty.Register(
        "ItemTemplateSelector", typeof(DataTemplateSelector), typeof(BindableCommandBarBehavior),
        new PropertyMetadata(null, null));

    public DataTemplateSelector ItemTemplateSelector
    {
        get { return (DataTemplateSelector)GetValue(ItemTemplateSelectorProperty); }
        set { SetValue(ItemTemplateSelectorProperty, value); }
    }

    public object PrimaryCommands
    {
        get { return  GetValue(PrimaryCommandsProperty); }
        set { SetValue(PrimaryCommandsProperty, value); }
    }

    protected override void OnDetaching()
    {
        base.OnDetaching();
        if (PrimaryCommands is INotifyCollectionChanged notifyCollectionChanged)
        {
            notifyCollectionChanged.CollectionChanged -= PrimaryCommandsCollectionChanged;
        }
    }

    private void UpdatePrimaryCommands()
    {
        if (AssociatedObject == null)
            return;

        if (PrimaryCommands == null)
            return;

        AssociatedObject.PrimaryCommands.Clear();

        if (!(PrimaryCommands is IEnumerable enumerable))
        {
            AssociatedObject.PrimaryCommands.Clear();
            return;
        }


        foreach (var command in enumerable)
        {
            var template = ItemTemplateSelector.SelectTemplate(command, AssociatedObject);

            if (!(template?.LoadContent() is FrameworkElement dependencyObject))
                continue;

            dependencyObject.DataContext = command;

            if (dependencyObject is ICommandBarElement icommandBarElement)
                AssociatedObject.PrimaryCommands.Add(icommandBarElement);
        }
    }

    protected override void OnAttached()
    {
        base.OnAttached();
        UpdatePrimaryCommands();
    }

    private void PrimaryCommandsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        UpdatePrimaryCommands();
    }

    private static void UpdateCommands(DependencyObject dependencyObject,
        DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs)
    {
        if (!(dependencyObject is BindableCommandBarBehavior behavior)) return;
        if (dependencyPropertyChangedEventArgs.OldValue is INotifyCollectionChanged oldList)
        {
            oldList.CollectionChanged -= behavior.PrimaryCommandsCollectionChanged;
        }

        if (dependencyPropertyChangedEventArgs.NewValue is INotifyCollectionChanged newList)
        {
            newList.CollectionChanged += behavior.PrimaryCommandsCollectionChanged;
        }

        behavior.UpdatePrimaryCommands();
    }
}

The DataTemplateSelector:

public class CommandBarMenuItemTemplateSelector : DataTemplateSelector
{
    public DataTemplate CbMenuItemTemplate { get; set; }


    protected override DataTemplate SelectTemplateCore(object item, DependencyObject container)
    {
        if (item is ContextAction)
        {
            return CbMenuItemTemplate;
        }

        return base.SelectTemplateCore(item, container);
    }
}

Xaml for templates:

 <DataTemplate x:Key="CbMenuItemTemplate">
    <AppBarButton
        Command="{Binding Command}"
        Icon="Add"
        Label="{Binding Text}" />
</DataTemplate>

<viewLogic:CommandBarMenuItemTemplateSelector x:Key="CommandBarMenuItemTemplateSelector" 
                                              CbMenuItemTemplate="{StaticResource CbMenuItemTemplate}" />

Usage:

  <CommandBar>
    <interactivity:Interaction.Behaviors>
       <viewLogic:BindableCommandBarBehavior ItemTemplateSelector="{StaticResource CommandBarMenuItemTemplateSelector}" PrimaryCommands="{Binding ContextActions}" />
    </interactivity:Interaction.Behaviors>
  </CommandBar>

Where ContextActions is a ObservableCollection of my class ContextAction.