内容设置在 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;
    }