Настраиваемый привязываемый stacklayout должен запускать пользовательский интерфейс при изменении элемента ObservableCollection

#c# #xaml #xamarin.forms

#c# #xaml #xamarin.forms

Вопрос:

У меня проблема с моим пользовательским stacklayout, который правильно заполняет stacklayout, но не распознает никаких изменений какого-либо элемента в связанной наблюдаемой коллекции..

Это код, который я использую для привязываемого stacklayout:

 public class BindableStackLayout : StackLayout
{
    private readonly Label _header;

    public BindableStackLayout()
    {
        _header = new Label();
        Children.Add(_header);
    }

    public IEnumerable ItemsSource
    {
        get => (IEnumerable)GetValue(ItemsSourceProperty);
        set => SetValue(ItemsSourceProperty, value);
    }

    public static readonly BindableProperty ItemsSourceProperty = BindableProperty.Create(nameof(ItemsSource), typeof(IEnumerable), 
        typeof(BindableStackLayout), propertyChanged: (bindable, oldValue, newValue) => ((BindableStackLayout)bindable).PopulateItems());

    public DataTemplate ItemDataTemplate
    {
        get => (DataTemplate)GetValue(ItemDataTemplateProperty);
        set => SetValue(ItemDataTemplateProperty, value);
    }

    public static readonly BindableProperty ItemDataTemplateProperty = BindableProperty.Create(nameof(ItemDataTemplate), 
        typeof(DataTemplate), typeof(BindableStackLayout));

    public string Title
    {
        get => (string)GetValue(TitleProperty);
        set => SetValue(TitleProperty, value);
    }

    public static readonly BindableProperty TitleProperty = BindableProperty.Create(nameof(Title), typeof(string), 
        typeof(BindableStackLayout), propertyChanged: (bindable, oldValue, newValue) => ((BindableStackLayout)bindable).PopulateHeader());

    private void PopulateItems()
    {
        if (ItemsSource == null)
            return;

        foreach (var item in ItemsSource)
        {
            var itemTemplate = ItemDataTemplate.CreateContent() as Xamarin.Forms.View;
            itemTemplate.BindingContext = item;
            Children.Add(itemTemplate);
        }
    }

    private void PopulateHeader() => _header.Text = Title;
}
  

Который используется, как вы можете найти здесь:

 <ContentView.Content>
    <h:BindableStackLayout ItemsSource="{Binding MenuHotKeys, Mode=TwoWay}"
                           Style="{StaticResource MenuControlStackLayout}">
        <h:BindableStackLayout.ItemDataTemplate>
            <DataTemplate>
                <Button Text="{Binding DataA}"
                        Command="{Binding Path=BindingContext.MenuControlCommand, Source={x:Reference InternalMenuControl}}"
                        CommandParameter="{Binding .}"
                        Style="{StaticResource MenuControlButton}"/>
            </DataTemplate>
        </h:BindableStackLayout.ItemDataTemplate>
    </h:BindableStackLayout>
</ContentView.Content>
  

И в viewmodel у меня есть этот код:

 private ObservableCollection<ConfigMenuItem> _menuHotKeys;
    public ObservableCollection<ConfigMenuItem> MenuHotKeys
    {
        get => _menuHotKeys;
        set => SetValue(ref _menuHotKeys, value);
    }
  

И изменение здесь:

 private async void MenuControlButtonPressed(object sender)
    {
        var menuItem = sender as ConfigMenuItem;

        if (menuItem.ItemId == _expanderId)
        {
            // toggle expanded menu visibility
            var expander = _menuHotKeys.FirstOrDefault(p => p.ItemId == _expanderId);

            var buffer = expander.DataA;
            expander.DataA = expander.DataB;
            expander.DataB = buffer;
        }
        else
        {
            await NavigationHandler.NavigateToMenuItem(menuItem);
        }
    }
  

Как вы можете видеть, я хочу переключить название кнопки привязки, но изменения не отображаются.

Я думаю, что мне нужно что-то изменить в привязываемом классе stacklayout, но что?

Maybe you can help

@INPC answers:
The ConfigMenuItem in the Collection derives from:

 public abstract class BaseObject : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    protected virtual void SetValue<T>(ref T field, T value, Expression<Func<T>> property)
    {
        if (!ReferenceEquals(field, value))
        {
            field = value;
            OnPropertyChanged(property);
        }
    }

    protected virtual void OnPropertyChanged<T>(Expression<Func<T>> changedProperty)
    {
        if (PropertyChanged != null)
        {
            string name = ((MemberExpression)changedProperty.Body).Member.Name;
            PropertyChanged(this, new PropertyChangedEventArgs(name));
        }
    }
}
  

и viewmodel является производным от:

 public abstract class BaseViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

    protected virtual void SetValue<T>(ref T privateField, T value, [CallerMemberName] string propertyName = null)
    {
        if (!EqualityComparer<T>.Default.Equals(privateField, value))
        {
            privateField = value;
            OnPropertyChanged(propertyName);
        }

        return;
    }
}
  

Как запрошено в комментариях к классу ConfigMenuItem, код BaseObject см. в коде вверх:

 public class ConfigMenuItem : BaseObject, IConfigMenuItem
{
    public int ItemId
    {
        get;
        set;
    }

    public int Position
    {
        get;
        set;
    }

    public string Name
    {
        get;
        set;
    }

    public string DataA
    {
        get;
        set;
    }

    public string DataB
    {
        get;
        set;
    }

    public bool IsEnabled
    {
        get;
        set;
    }

    public bool IsHotKey
    {
        get;
        set;
    }

    public bool IsCustomMenuItem
    {
        get;
        set;
    }

    public override string ToString()
    {
        return $"{Name} ({DataA} | {DataB ?? "null"})";
    }
}
  

Комментарии:

1. Изменилось ли ваше значение Set для MenuHotKeys уведомления о свойстве?

2. ObservableCollection уведомляет только об изменениях в коллекции, то есть о Добавлении и удалении. Изменения свойств в каждом элементе коллекции должны обрабатываться путем реализации INPC в классе item

3. Значение Set для MenuHotKeys является производным от INPC (вопрос обновлен).. Также модель в коллекции является производной от класса INPC .. это отлично работает с обычными списками и другим поведением, поэтому я считаю, что моя проблема связана с классом bindablestacklayout .. потому что есть только метод для заполнения.. может быть, я могу зарегистрировать класс для повторного заполнения при изменении элемента списка или чего-либо еще?

4. Пожалуйста, опубликуйте исходный код ConfigMenuItem класса.

5. Я думаю, что проблема, скорее всего, там, а не в BindableStackLayout классе.

Ответ №1:

Проблема вызвана тем фактом, что, хотя ваш ConfigMenuItem класс является производным от BaseObject , все его свойства являются обычными свойствами и не вызывают PropertyChanged событие. Вам нужно переписать свойства, чтобы иметь резервное поле и запускать событие в их установщике. Например:

 private string _dataA;
public string DataA
{
    get => _dataA;
    set => SetValue(ref _dataA, value);
}
  

В моем примере используется SetValue метод из BaseViewModel , и я действительно думаю, что BaseObject класс избыточен, и вы могли бы просто использовать BaseViewModel вместо этого. Использование [CallerMemberName] свойства for намного удобнее, чем наличие дополнительной логики для Expression<Func<T>> .

Комментарии:

1. Спасибо за вашу помощь! Я думал, что я что-то напутал.. были внесены изменения, которые вы рекомендовали, и теперь это работает, как ожидалось!

2. Потрясающе, рад, что это помогло 🙂