#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. Потрясающе, рад, что это помогло 🙂