XAML Binding within a resource dictionary (ContentPage.Resources) does not work

MartinRobins.0464MartinRobins.0464 GBMember
edited September 2015 in Xamarin.Forms

I am creating a form using MVVM. In general it works, however I want to bind the Command property of each item in a ListView control to an ICommand on the view model, shown below (the property I need to bind to being the "NavigateCommand" property)...

    public class HomePageViewModel {

        private Command<Type> navigateCommand;
        private ObservableCollection<Menu> menus;

        public Command<Type> NavigateCommand {
            get {

                if (this.navigateCommand == null)
                    this.navigateCommand = new Command<Type>(async (Type type) => {

                        Page page = (Page)Activator.CreateInstance(type);
                        await Application.Current.MainPage.Navigation.PushAsync(page);

                    });

                return this.navigateCommand;
            }
        }

        public ObservableCollection<Menu> Menus {
            get {

                if (this.menus == null) {

                    this.menus = new ObservableCollection<Menu> {
                    };

                }

                return this.menus;

            }
        }

        public class Menu {

            public ObservableCollection<MenuItem> MenuItems { get; set; }

            public String Title { get; set; }

        }

        public class MenuItem {

            public String Detail { get; set; }

            public Object ImageSource { get; set; }

            public Type PageType { get; set; }

            public String Text { get; set; }

        }

    }

In order to achieve this, I have adapted a technique that I have used many times in WPF; I have created what I call a "CommandReference" object that can be created as a page level resource with a reference to the original command and then bound to within the form as a static resource...

    public class CommandReference : BindableObject, ICommand {

        public static readonly BindableProperty CommandProperty = 
            BindableProperty.Create("Command", typeof(ICommand), typeof(CommandReference),
            defaultValue: null, propertyChanged: CommandReference.OnCommandChanged);

        public ICommand Command {
            get { return (ICommand)GetValue(CommandReference.CommandProperty); }
            set { SetValue(CommandReference.CommandProperty, value); }
        }

        public event EventHandler CanExecuteChanged;

        public bool CanExecute(object parameter) {
            if (Command != null)
                return Command.CanExecute(parameter);
            return false;
        }

        public void Execute(object parameter) {
            if (this.CanExecute(parameter)) {
                Command.Execute(parameter);
            }
        }

        private static void OnCommandChanged(BindableObject bindable, object oldValue, object newValue) {

            CommandReference commandReference = bindable as CommandReference;
            ICommand oldCommand = oldValue as ICommand;
            ICommand newCommand = newValue as ICommand;

            if (oldCommand != null)
                oldCommand.CanExecuteChanged -= commandReference.CanExecuteChanged;

            if (newCommand != null)
                newCommand.CanExecuteChanged += commandReference.CanExecuteChanged;

        }

    }

I then add this to my XAML...

    <?xml version="1.0" encoding="utf-8" ?>
    <xlfc:ExtendedTabbedPage x:Class="xx.Views.HomePage"
        xmlns="http://xamarin.com/schemas/2014/forms"
        xmlns:local="clr-namespace:xx"
        xmlns:viewModels="clr-namespace:xx.ViewModels"
        xmlns:views="clr-namespace:xx.Views"
        xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
        xmlns:xlfc="clr-namespace:XLabs.Forms.Controls"
        ItemsSource="{Binding Menus}"
        Title="{x:Static local:Constants.ApplicationTitle}">

        <ContentPage.Resources>
            <ResourceDictionary>
                <views:CommandReference x:Key="navigateCommand"
                    Command="{Binding NavigateCommand}"/>
            </ResourceDictionary>
        </ContentPage.Resources>

        <ContentPage.BindingContext>
            <viewModels:HomePageViewModel />
        </ContentPage.BindingContext>

        <xlfc:ExtendedTabbedPage.ItemTemplate>
            <DataTemplate>
                <ContentPage Title="{Binding Title}">
                    <ListView HorizontalOptions="StartAndExpand"
                        ItemsSource="{Binding MenuItems}"
                        VerticalOptions="StartAndExpand">
                        <ListView.ItemTemplate>
                            <DataTemplate>
                                <ImageCell Command="{StaticResource navigateCommand}"
                                    CommandParameter="{Binding PageType}"
                                    Detail="{Binding Detail}"
                                    ImageSource="{Binding ImageSource}"
                                    Text="{Binding Text}"/>
                            </DataTemplate>
                        </ListView.ItemTemplate>
                    </ListView>
                </ContentPage>
            </DataTemplate>
        </xlfc:ExtendedTabbedPage.ItemTemplate>

    </xlfc:ExtendedTabbedPage>

All works well from a UI binding perspective with the tabs and menus being displayed as expected and the binding from the ImageCell to the CommandReference being exactly as expected, however when the CommandReference instance is created it is never bound to the "NavigationCommand" property of the view model. I do not understand why this is the case.

Can any body therefore explain why the binding (during instantiation of the CommandReference object as a page resource) is never actioned?

I am using Xamarin.Forms version 1.4.3.6376 if that makes a difference and I have tested this on both Android and Windows so far.

Thanks for any help you can offer.

Martin.

Best Answer

Answers

  • You are quite correct; resources are not provided with a BindingContext. I modified by form code-behind to assign a context to any resource of type BindableObject and now all works...

    protected override void OnBindingContextChanged() {
        base.OnBindingContextChanged();
    
        if (this.Resources != null) {
            foreach (var resource in this.Resources.Values.OfType<BindableObject>()) {
                resource.BindingContext = this.BindingContext;
            }
        }
    
    }
    
  • RichardEdwardsRichardEdwards CAUniversity ✭✭

    @MartinRobins.0464
    Thanks for this, works brilliantly. I needed a way to bind a viewmodel level command to a custom viewcell tap gesture and this worked great.

    I had to change the code slightly because I was getting an error with OfType on the collection but works great now.

        protected override void OnBindingContextChanged() {
            base.OnBindingContextChanged();
    
            if (this.Resources != null) {
                foreach (var resource in this.Resources.Values) {
                    if (resource is BindableObject)
                        ((BindableObject) resource).BindingContext = this.BindingContext;
                }
            }
    
        }
    
Sign In or Register to comment.