如何使视图模型绑定与自定义的Shell应用标题视图一起工作 – Xamarin Forms

huangapple go评论77阅读模式
英文:

How to make view model bindings work with custom Shell App Title View - Xamarin Forms

问题

I've extracted the main content of your request for translation:

"I'm trying to make a customizable, re-usable Title View template for my Xamarin Forms Shell app. By 'Title View,' I mean the title bar that appears at the top of a page. I've mostly implemented it, but I'm struggling to make some bindings work.

The idea is pretty simple. I want to end up with something that looks like this, a pretty standard title bar template where I can fill in the blanks on a page-by-page basis:

如何使视图模型绑定与自定义的Shell应用标题视图一起工作 – Xamarin Forms

And I want to be able to create it like this in any of my ContentPages. I've rigged it so that I only need to tell the buttons what their image source and commands are, and the rest will be formatted for me:

<ContentPage
    x:DataType="viewmodels:MyViewModel"
    ... >

    <Shell.TitleView>
        
        <tv:CustomAppShellTitleView>
                        
            <tv:CustomAppShellTitleView.LeftButton>
                <tv:TitleViewButtonInfo Command="{Binding SomeCommandHere}" Source="{StaticResource SomeIconHere}" />
            </tv:CustomAppShellTitleView.LeftButton>

            <tv:CustomAppShellTitleView.Title>SomeTitle</tv:CustomAppShellTitleView.Title>

            <tv:CustomAppShellTitleView.RightButton1>
                <tv:TitleViewButtonInfo Command="{Binding SomeCommandHere}" Source="{StaticResource SomeIconHere}" />
            </tv:CustomAppShellTitleView.RightButton1>

            <tv:CustomAppShellTitleView.RightButton2>
                <tv:TitleViewButtonInfo Command="{Binding SomeCommandHere}" Source="{StaticResource SomeIconHere}" />
            </tv:CustomAppShellTitleView.RightButton2>

            <tv:CustomAppShellTitleView.RightButton3>
                <tv:TitleViewButtonInfo Command="{Binding SomeCommandHere}" Source="{StaticResource SomeIconHere}" />
            </tv:CustomAppShellTitleView.RightButton3>
            
        </tv:CustomAppShellTitleView>
        
    </Shell.TitleView>
...

All of my ContentPages use view models, and I want to use data from them to configure the title bar behavior (e.g., button commands, visibility, etc).

TitleViewButtonInfo
You'll notice that I've used something called 'TitleViewButtonInfo.' I figured that since I've got 4 possible buttons, I could reduce code duplication by putting their Bindable Properties in a nice little object, like so:

using System.Windows.Input;
using Xamarin.Forms;

namespace MyNamespace.Views.TitleViews
{
    public class TitleViewButtonInfo : BindableObject
    {
        public static readonly BindableProperty CommandProperty = BindableProperty.Create(nameof(Command), typeof(ICommand), typeof(TitleViewButtonInfo), default(ICommand));
        public ICommand Command
        {
            get => (ICommand)GetValue(CommandProperty);
            set => SetValue(CommandProperty, value);
        }

        public static readonly BindableProperty SourceProperty = BindableProperty.Create(nameof(Source), typeof(ImageSource), typeof(TitleViewButtonInfo), default(ImageSource));
        public ImageSource Source
        {
            get => (ImageSource)GetValue(SourceProperty);
            set
            {
                SetValue(SourceProperty, value);
                OnPropertyChanged(nameof(IsVisible));
            }
        }

        public bool IsVisible => Source != null;
    }
}

CustomAppShellTitleView
And lastly, I've got my actual 'CustomAppShellTitleView.' I'm going to omit the right-side buttons for simplicity:

XAML

<ContentView
    x:Class="MyNamespace.Views.TitleViews.CustomAppShellTitleView"
    xmlns="http://xamarin.com/schemas/2014/forms"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    xmlns:local="clr-namespace:MyNamespace.Views.TitleViews"
    x:Name="customAppShellTitleView"
    x:DataType="local:CustomAppShellTitleView">
    <ContentView.Resources>
        <ResourceDictionary>
            <x:Double x:Key="ButtonSize">32</x:Double>
        </ResourceDictionary>
    </ContentView.Resources>
    <ContentView.Content>
        <Grid
            Padding="0,0,10,0"
            RowSpacing="0"
            VerticalOptions="CenterAndExpand">

            <Grid.ColumnDefinitions>
                <!--  Left-side Button  -->
                <ColumnDefinition Width="Auto" />
                <!--  Title  -->
                <ColumnDefinition Width="*" />
                <!--  Right-side Buttons  -->
                <ColumnDefinition Width="Auto" />
            </Grid.ColumnDefinitions>

            <!--  Left-side Button  -->
            <ImageButton
                Grid.Column="0"
                Command="{Binding LeftButton.Command, Source={x:Reference Name=customAppShellTitleView}}"
                HeightRequest="{StaticResource ButtonSize}"
                HorizontalOptions="Start"
                IsVisible="{Binding LeftButton.IsVisible, Source={x:Reference Name=customAppShellTitleView}}"
                Source="{Binding LeftButton.Source, Source={x:Reference Name=customAppShellTitleView}}"
                WidthRequest="{StaticResource ButtonSize}" />

            <!--  Title  -->
            <Label
                Grid.Column="1"
                HorizontalOptions="StartAndExpand"
                LineBreakMode="TailTruncation"
                MaxLines="1"
                Text="{Binding Title, Source={x:Reference Name=customAppShellTitleView}}"
                VerticalOptions="CenterAndExpand" />

            <!--  Right-side Buttons  -->
            <StackLayout
                Grid.Column="2"
                Orientation="Horizontal"
                Spacing="10">
                <!--  Buttons 1, 2 and 3 here -->
            </StackLayout>
        </Grid>
    </ContentView.Content>
</ContentView>

Code Behind

using Xamarin.Forms;
using Xamarin.Forms.Xaml;

namespace MyNamespace.Views.TitleViews
{
    [XamlCompilation(XamlCompilationOptions.Compile)]
    public partial class CustomAppShellTitleView : ContentView
    {
        // LeftButton
        public static readonly BindableProperty LeftButtonProperty = BindableProperty.Create(
            nameof(LeftButton),
            typeof(TitleViewButtonInfo),
            typeof(CustomAppShellTitleView),
            new TitleViewButtonInfo());

        public TitleViewButtonInfo LeftButton
        {
            get => (TitleViewButtonInfo)GetValue(LeftButtonProperty);
            set => SetValue(LeftButtonProperty, value);
        }

        // Same thing for RightButton1, 2 and 3

        #region Title Property
        public static readonly BindableProperty TitleProperty = BindableProperty.Create(nameof(Title), typeof(string), typeof(CustomAppShellTitleView), default(string));
        
        public string Title
        {
            get => (string)GetValue(TitleProperty);
            set => SetValue(TitleProperty, value);
        }
        #endregion

        public CustomAppShellTitleView()
        {
            InitializeComponent();
            BindingContext = this;
        }
    }
}

The Problem
This setup seems to work for the most part. Everything looks nice. The title and button image sources are being set properly at runtime, mainly because they are static values that exist at the time the Shell.TitleView is set in my page's XAML. However, the Bindings are the issue.

英文:

Context

I'm trying to make a customizable, re-usable Title View template for my Xamarin Forms Shell app. By "Title View", I mean the title bar that appears at the top of a page. I've mostly implemented it, but I'm struggling to make some bindings work.

The idea is pretty simple. I want to end up with something that looks like this, a pretty standard title bar template where I can fill in the blanks on a page-by-page basis:

如何使视图模型绑定与自定义的Shell应用标题视图一起工作 – Xamarin Forms

And I want to be able to create it like this in any of my ContentPages. I've rigged it so that I only need to tell the buttons what their image source and commands are, and the rest will be formatted for me:

&lt;ContentPage
    x:DataType=&quot;viewmodels:MyViewModel&quot;
    ... &gt;

    &lt;Shell.TitleView&gt;
        
        &lt;tv:CustomAppShellTitleView&gt;
                        
            &lt;tv:CustomAppShellTitleView.LeftButton&gt;
                &lt;tv:TitleViewButtonInfo Command=&quot;{Binding SomeCommandHere}&quot; Source=&quot;{StaticResource SomeIconHere}&quot; /&gt;
            &lt;/tv:CustomAppShellTitleView.LeftButton&gt;

            &lt;tv:CustomAppShellTitleView.Title&gt;SomeTitle&lt;/tv:CustomAppShellTitleView.Title&gt;

            &lt;tv:CustomAppShellTitleView.RightButton1&gt;
                &lt;tv:TitleViewButtonInfo Command=&quot;{Binding SomeCommandHere}&quot; Source=&quot;{StaticResource SomeIconHere}&quot; /&gt;
            &lt;/tv:CustomAppShellTitleView.RightButton1&gt;

            &lt;tv:CustomAppShellTitleView.RightButton2&gt;
                &lt;tv:TitleViewButtonInfo Command=&quot;{Binding SomeCommandHere}&quot; Source=&quot;{StaticResource SomeIconHere}&quot; /&gt;
            &lt;/tv:CustomAppShellTitleView.RightButton2&gt;

            &lt;tv:CustomAppShellTitleView.RightButton3&gt;
                &lt;tv:TitleViewButtonInfo Command=&quot;{Binding SomeCommandHere}&quot; Source=&quot;{StaticResource SomeIconHere}&quot; /&gt;
            &lt;/tv:CustomAppShellTitleView.RightButton3&gt;
            
        &lt;/tv:CustomAppShellTitleView&gt;
        
    &lt;/Shell.TitleView&gt;
...

All of my ContentPages use view models, and I want to use data from them to configure the title bar behaviour (e.g., button commands, visibily, etc).

TitleViewButtonInfo

You'll notice that I've used something called TitleViewButtonInfo. I figured that since I've got 4 possible buttons, I could reduce code duplication by putting their Bindable Properties in a nice little object, like so:

using System.Windows.Input;
using Xamarin.Forms;

namespace MyNamespace.Views.TitleViews
{
    public class TitleViewButtonInfo : BindableObject
    {
        public static readonly BindableProperty CommandProperty = BindableProperty.Create(nameof(Command), typeof(ICommand), typeof(TitleViewButtonInfo), default(ICommand));
        public ICommand Command
        {
            get =&gt; (ICommand)GetValue(CommandProperty);
            set =&gt; SetValue(CommandProperty, value);
        }

        public static readonly BindableProperty SourceProperty = BindableProperty.Create(nameof(Source), typeof(ImageSource), typeof(TitleViewButtonInfo), default(ImageSource));
        public ImageSource Source
        {
            get =&gt; (ImageSource)GetValue(SourceProperty);
            set
            {
                SetValue(SourceProperty, value);
                OnPropertyChanged(nameof(IsVisible));
            }
        }

        public bool IsVisible =&gt; Source != null;
    }
}

CustomAppShellTitleView

And lastly, I've got my actual CustomAppShellTitleView. I'm going to omit the right-side buttons for simplicity:

XAML

&lt;ContentView
    x:Class=&quot;MyNamespace.Views.TitleViews.CustomAppShellTitleView&quot;
    xmlns=&quot;http://xamarin.com/schemas/2014/forms&quot;
    xmlns:x=&quot;http://schemas.microsoft.com/winfx/2009/xaml&quot;
    xmlns:local=&quot;clr-namespace:MyNamespace.Views.TitleViews&quot;
    x:Name=&quot;customAppShellTitleView&quot;
    x:DataType=&quot;local:CustomAppShellTitleView&quot;&gt;
    &lt;ContentView.Resources&gt;
        &lt;ResourceDictionary&gt;
            &lt;x:Double x:Key=&quot;ButtonSize&quot;&gt;32&lt;/x:Double&gt;
        &lt;/ResourceDictionary&gt;
    &lt;/ContentView.Resources&gt;
    &lt;ContentView.Content&gt;
        &lt;Grid
            Padding=&quot;0,0,10,0&quot;
            RowSpacing=&quot;0&quot;
            VerticalOptions=&quot;CenterAndExpand&quot;&gt;

            &lt;Grid.ColumnDefinitions&gt;
                &lt;!--  Left-side Button  --&gt;
                &lt;ColumnDefinition Width=&quot;Auto&quot; /&gt;
                &lt;!--  Title  --&gt;
                &lt;ColumnDefinition Width=&quot;*&quot; /&gt;
                &lt;!--  Right-side Buttons  --&gt;
                &lt;ColumnDefinition Width=&quot;Auto&quot; /&gt;
            &lt;/Grid.ColumnDefinitions&gt;

            &lt;!--  Left-side Button  --&gt;
            &lt;ImageButton
                Grid.Column=&quot;0&quot;
                Command=&quot;{Binding LeftButton.Command, Source={x:Reference Name=customAppShellTitleView}}&quot;
                HeightRequest=&quot;{StaticResource ButtonSize}&quot;
                HorizontalOptions=&quot;Start&quot;
                IsVisible=&quot;{Binding LeftButton.IsVisible, Source={x:Reference Name=customAppShellTitleView}}&quot;
                Source=&quot;{Binding LeftButton.Source, Source={x:Reference Name=customAppShellTitleView}}&quot;
                WidthRequest=&quot;{StaticResource ButtonSize}&quot; /&gt;

            &lt;!--  Title  --&gt;
            &lt;Label
                Grid.Column=&quot;1&quot;
                HorizontalOptions=&quot;StartAndExpand&quot;
                LineBreakMode=&quot;TailTruncation&quot;
                MaxLines=&quot;1&quot;
                Text=&quot;{Binding Title, Source={x:Reference Name=customAppShellTitleView}}&quot;
                VerticalOptions=&quot;CenterAndExpand&quot; /&gt;

            &lt;!--  Right-side Buttons  --&gt;
            &lt;StackLayout
                Grid.Column=&quot;2&quot;
                Orientation=&quot;Horizontal&quot;
                Spacing=&quot;10&quot;&gt;
                &lt;!--  Buttons 1, 2 and 3 here --&gt;
            &lt;/StackLayout&gt;
        &lt;/Grid&gt;
    &lt;/ContentView.Content&gt;
&lt;/ContentView&gt;

Code Behind

using Xamarin.Forms;
using Xamarin.Forms.Xaml;

namespace MyNamespace.Views.TitleViews
{
    [XamlCompilation(XamlCompilationOptions.Compile)]
    public partial class CustomAppShellTitleView : ContentView
    {
		// LeftButton
		public static readonly BindableProperty LeftButtonProperty = BindableProperty.Create(
			nameof(LeftButton),
			typeof(TitleViewButtonInfo),
			typeof(CustomAppShellTitleView),
			new TitleViewButtonInfo());

		public TitleViewButtonInfo LeftButton
		{
			get =&gt; (TitleViewButtonInfo)GetValue(LeftButtonProperty);
			set =&gt; SetValue(LeftButtonProperty, value);
		}

		// Same thing for RightButton1, 2 and 3

		#region Title Property
		public static readonly BindableProperty TitleProperty = BindableProperty.Create(nameof(Title), typeof(string), typeof(CustomAppShellTitleView), default(string));
		
		public string Title
		{
			get =&gt; (string)GetValue(TitleProperty);
			set =&gt; SetValue(TitleProperty, value);
		}
		#endregion

		public CustomAppShellTitleView()
		{
			InitializeComponent();
			BindingContext = this;
		}
	}
}

The Problem

This setup seems to work for the most part. Everything looks nice. The title and button image sources are being set properly at runtime, mainly because they are static values that exist at the time the Shell.TitleView is set in my page's XAML. However, the Bindings are the issue. For instance, the buttons do nothing when they are pressed despite having their Command properties bound to some ICommand in my view model. This command works fine when I bind it to a regular button in my view, so it's not due to some mismatch between my XAML and view model.

There are clearly some fundamental things I don't understand here.

What I've Tried

I set breakpoints in the TitleViewButtonInfo.CommandProperty and my view model's constructor where the commands are being assigned. Since the view is initialized prior to the view model being set as the BindingContext (or even existing), this makes sense -- but the setter for the CommandProperty is never hit again once the view model has actually set it. Obviously, the first time it hits, the value will be null since the view model hasn't initialized yet. Thus, even when that ICommand is set in my view model, the title view doesn't hear it. I tried to trigger an OnPropertyChanged on the Command that was bound, but that didn't work.

Is there any way to make it hear when the Command in my view model is set? There must be something obvious that I'm doing wrong to make it such that the properties of this title view can only be set once and never again.

答案1

得分: 1

The BindingContext for TitleViewButtonInfo is not the ViewModel for the page. So you may set the binding source for it. Consider the following code:

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
...
x:Name="this">

<Shell.TitleView>
<tv:CustomAppShellTitleView>
<tv:CustomAppShellTitleView.LeftButton>
<tv:TitleViewButtonInfo Command="{Binding BindingContext.LeftButtonCommand,Source={x:Reference this}}" ... />
</tv:CustomAppShellTitleView.LeftButton>

Hope it works.

英文:

The BindingContext for TitleViewButtonInfo is not the ViewModel for the page. So you may set the binding source for it. Consider the following code:

&lt;ContentPage xmlns=&quot;http://schemas.microsoft.com/dotnet/2021/maui&quot;
         ...
         x:Name=&quot;this&quot;&gt;

&lt;Shell.TitleView&gt;
    &lt;tv:CustomAppShellTitleView&gt;
        &lt;tv:CustomAppShellTitleView.LeftButton&gt;
            &lt;tv:TitleViewButtonInfo Command=&quot;{Binding BindingContext.LeftButtonCommand,Source={x:Reference this}}&quot; ... /&gt;
        &lt;/tv:CustomAppShellTitleView.LeftButton&gt;

Hope it works.

huangapple
  • 本文由 发表于 2023年5月17日 06:41:17
  • 转载请务必保留本文链接:https://go.coder-hub.com/76267515.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定