英文:
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:
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:
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 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 => (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. 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:
<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.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论