内容设置在 XAML 中的 IList<View> 类型的 BindableProperty 永远不会更新
BindableProperty of type IList<View> with contents set in XAML is never updated
我正在为我在整个应用程序中使用的常见容器类型制作自定义 ContentView。自定义视图有两个可绑定属性:显示在左上角的header(字符串)和显示在右上角的按钮列表(List)
header属性(和内容 属性)按预期工作,但按钮 属性 根本不起作用。 ButtonsPropertyChanged
永远不会触发,并且输出控制台中没有绑定错误消息。我做错了什么?
HomePage.xaml:
<controls:SectionContainer Header="Text blah"> <!-- Header works -->
<controls:SectionContainer.Buttons>
<!-- This does NOT work - buttons are not shown -->
<ImageButton Source="{Binding SomeIcon}"></ImageButton>
<ImageButton Source="{Binding AnotherIcon}"></ImageButton>
</controls:SectionContainer.Buttons>
<!-- Actual contents here -- this works, the contents are shown -->
</controls:SectionContainer>
SectionContainer.xaml:
<ContentView.Content>
<Frame Style="{StaticResource SectionFrame}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"></ColumnDefinition>
<ColumnDefinition Width="Auto"></ColumnDefinition>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="30"></RowDefinition>
<RowDefinition Height="*"></RowDefinition>
</Grid.RowDefinitions>
<Label x:Name="HeaderLabel" Grid.Column="0" Grid.Row="0" VerticalTextAlignment="Start" VerticalOptions="Start" Style="{StaticResource Header}"></Label>
<StackLayout x:Name="ButtonsContainer" Grid.Row="0" Grid.Column="1" Orientation="Horizontal" HorizontalOptions="End"></StackLayout>
<StackLayout Grid.Column="0" Grid.Row="1" x:Name="Container" Style="{StaticResource Section}"></StackLayout>
</Grid>
</Frame>
</ContentView.Content>
SectionContainer.xaml.cs:
[XamlCompilation(XamlCompilationOptions.Compile)]
[ContentProperty("Contents")]
public partial class SectionContainer : ContentView
{
public IList<View> Contents => Container.Children;
public string Header
{
get => (string)GetValue(HeaderProperty);
set => SetValue(HeaderProperty, value);
}
public static readonly BindableProperty HeaderProperty = BindableProperty.Create(
nameof(Header),
typeof(string),
typeof(SectionContainer),
"",
propertyChanged: HeaderPropertyChanged);
private static void HeaderPropertyChanged(BindableObject bindable, object oldvalue, object newvalue)
{
((SectionContainer) bindable).HeaderLabel.Text = (string) newvalue;
}
public IList<View> Buttons
{
get => (IList<View>)GetValue(ButtonsProperty);
set => SetValue(ButtonsProperty, value);
}
public static readonly BindableProperty ButtonsProperty = BindableProperty.Create(
nameof(Buttons),
typeof(IList<View>),
typeof(SectionContainer),
new List<View>(),
BindingMode.OneWay,
propertyChanged: ButtonsPropertyChanged);
private static void ButtonsPropertyChanged(BindableObject bindable, object oldvalue, object newvalue)
{
Debug.WriteLine(newvalue); // This is never printed, and there are no error messages
var children= ((SectionContainer)bindable).ButtonsContainer.Children;
children.Clear();
foreach (var child in (IList<View>) newvalue)
children.Add(child);
}
public SectionContainer()
{
InitializeComponent();
}
}
Xamarin.Forms 不设置 属性,它向作为默认值创建的对象添加元素。
因此,使用 defaultValueCreator
(而不是 defaultValue
)创建的 ObservableCollection
,我们在其中侦听更改并填充用户控件似乎有效。不确定我们是否需要 propertyChanged
事件,或者我们是否需要处理除单个元素添加之外的任何其他 ObservableCollection
事件,但我保留它以防万一。
public static readonly BindableProperty ButtonsProperty = BindableProperty.Create(
nameof(Buttons),
typeof(IList<View>),
typeof(SectionContainer),
propertyChanged: ButtonsPropertyChanged,
defaultValueCreator: CreateObservableList);
private static object CreateObservableList(BindableObject bindable)
{
var oc = new ObservableCollection<View>();
// It appears Xamarin.Forms doesn't create a list containing the Views from the XAML, it uses the default value (list),
// and adds the Views one by one. Therefore, we create the default value as an ObservableCollection, then we listen
// for changes to this collection.
oc.CollectionChanged += (o, e) =>
{
if (e.Action == NotifyCollectionChangedAction.Add)
{
// If this is an Add operation, simply add the view to the StackLayout.
var children = ((SectionContainer) bindable).ButtonsContainer.Children;
foreach (var child in e.NewItems.Cast<View>())
children.Add(child);
}
else
{
// Otherwise, recreate the StackLayout by clearing it, and adding all children all over again.
// (Won't bother handling all cases, since the normal one seems to be Add anyway.)
ButtonsPropertyChanged(bindable, null, o);
}
};
return oc;
}
我正在为我在整个应用程序中使用的常见容器类型制作自定义 ContentView。自定义视图有两个可绑定属性:显示在左上角的header(字符串)和显示在右上角的按钮列表(List)
header属性(和内容 属性)按预期工作,但按钮 属性 根本不起作用。 ButtonsPropertyChanged
永远不会触发,并且输出控制台中没有绑定错误消息。我做错了什么?
HomePage.xaml:
<controls:SectionContainer Header="Text blah"> <!-- Header works -->
<controls:SectionContainer.Buttons>
<!-- This does NOT work - buttons are not shown -->
<ImageButton Source="{Binding SomeIcon}"></ImageButton>
<ImageButton Source="{Binding AnotherIcon}"></ImageButton>
</controls:SectionContainer.Buttons>
<!-- Actual contents here -- this works, the contents are shown -->
</controls:SectionContainer>
SectionContainer.xaml:
<ContentView.Content>
<Frame Style="{StaticResource SectionFrame}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"></ColumnDefinition>
<ColumnDefinition Width="Auto"></ColumnDefinition>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="30"></RowDefinition>
<RowDefinition Height="*"></RowDefinition>
</Grid.RowDefinitions>
<Label x:Name="HeaderLabel" Grid.Column="0" Grid.Row="0" VerticalTextAlignment="Start" VerticalOptions="Start" Style="{StaticResource Header}"></Label>
<StackLayout x:Name="ButtonsContainer" Grid.Row="0" Grid.Column="1" Orientation="Horizontal" HorizontalOptions="End"></StackLayout>
<StackLayout Grid.Column="0" Grid.Row="1" x:Name="Container" Style="{StaticResource Section}"></StackLayout>
</Grid>
</Frame>
</ContentView.Content>
SectionContainer.xaml.cs:
[XamlCompilation(XamlCompilationOptions.Compile)]
[ContentProperty("Contents")]
public partial class SectionContainer : ContentView
{
public IList<View> Contents => Container.Children;
public string Header
{
get => (string)GetValue(HeaderProperty);
set => SetValue(HeaderProperty, value);
}
public static readonly BindableProperty HeaderProperty = BindableProperty.Create(
nameof(Header),
typeof(string),
typeof(SectionContainer),
"",
propertyChanged: HeaderPropertyChanged);
private static void HeaderPropertyChanged(BindableObject bindable, object oldvalue, object newvalue)
{
((SectionContainer) bindable).HeaderLabel.Text = (string) newvalue;
}
public IList<View> Buttons
{
get => (IList<View>)GetValue(ButtonsProperty);
set => SetValue(ButtonsProperty, value);
}
public static readonly BindableProperty ButtonsProperty = BindableProperty.Create(
nameof(Buttons),
typeof(IList<View>),
typeof(SectionContainer),
new List<View>(),
BindingMode.OneWay,
propertyChanged: ButtonsPropertyChanged);
private static void ButtonsPropertyChanged(BindableObject bindable, object oldvalue, object newvalue)
{
Debug.WriteLine(newvalue); // This is never printed, and there are no error messages
var children= ((SectionContainer)bindable).ButtonsContainer.Children;
children.Clear();
foreach (var child in (IList<View>) newvalue)
children.Add(child);
}
public SectionContainer()
{
InitializeComponent();
}
}
Xamarin.Forms 不设置 属性,它向作为默认值创建的对象添加元素。
因此,使用 defaultValueCreator
(而不是 defaultValue
)创建的 ObservableCollection
,我们在其中侦听更改并填充用户控件似乎有效。不确定我们是否需要 propertyChanged
事件,或者我们是否需要处理除单个元素添加之外的任何其他 ObservableCollection
事件,但我保留它以防万一。
public static readonly BindableProperty ButtonsProperty = BindableProperty.Create(
nameof(Buttons),
typeof(IList<View>),
typeof(SectionContainer),
propertyChanged: ButtonsPropertyChanged,
defaultValueCreator: CreateObservableList);
private static object CreateObservableList(BindableObject bindable)
{
var oc = new ObservableCollection<View>();
// It appears Xamarin.Forms doesn't create a list containing the Views from the XAML, it uses the default value (list),
// and adds the Views one by one. Therefore, we create the default value as an ObservableCollection, then we listen
// for changes to this collection.
oc.CollectionChanged += (o, e) =>
{
if (e.Action == NotifyCollectionChangedAction.Add)
{
// If this is an Add operation, simply add the view to the StackLayout.
var children = ((SectionContainer) bindable).ButtonsContainer.Children;
foreach (var child in e.NewItems.Cast<View>())
children.Add(child);
}
else
{
// Otherwise, recreate the StackLayout by clearing it, and adding all children all over again.
// (Won't bother handling all cases, since the normal one seems to be Add anyway.)
ButtonsPropertyChanged(bindable, null, o);
}
};
return oc;
}