Настраиваемая привязка стека должна вызывать пользовательский интерфейс при изменении элемента ObservableCollection - PullRequest
0 голосов
/ 18 марта 2019

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

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

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);
        }
    }

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

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

Может быть, вы можете помочь

@ INPC ответы: ConfigMenuItem в Коллекции происходит из:

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));
        }
    }
}

, а модель представления происходит из:

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 Ответ

1 голос
/ 18 марта 2019

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

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...