Мне нужно программно получить все ячейки просмотра моего списка, чтобы я мог изменить цвет фона определенного дочернего макета в ячейке просмотра. Нет проблем с обновлением цвета для ячейки просмотра при нажатии, но мне нужно изменить цвет всех ячеек просмотра на цвет по умолчанию всякий раз, когда нажимается другая ячейка просмотра.
При поиске решения я часто находил ответы, в которых доступ к ячейкам просмотра осуществлялся через свойства времени выполнения списка (см. код ниже или второй ответ здесь: Xamarin.Forms: получить все ячейки/элементы списка.), при тестировании кода я понял, что это не работает для списков, где группировка включена.
IEnumerable<PropertyInfo> pInfos = (connectionsListView as ItemsView<Cell>).GetType().GetRuntimeProperties();
var templatedItems = pInfos.FirstOrDefault(info => info.Name == "TemplatedItems");
if (templatedItems != null)
{
var cells = templatedItems.GetValue(connectionsListView);
foreach (ViewCell cell in cells as Xamarin.Forms.ITemplatedItemsList<Xamarin.Forms.Cell>)
{
if (cell.BindingContext != null && cell.BindingContext is MyModelClass)
{
// Change background color of viewcell
}
}
}
Когда группировка включена, этот код возвращает только сгруппированные ячейки просмотра заголовка. Я не смог найти ответа, чтобы изменить этот код, поэтому вместо заголовков возвращаются фактические ячейки просмотра «тела». Есть ли какой-либо возможный способ изменить этот код, чтобы я получил ожидаемый результат, или мне нужно использовать для этого собственный рендерер?
Здесь вы можете увидеть список, который я сейчас использую в своем XAML. Я пытаюсь найти решение, в котором я могу привязать цвет фона каждой ячейки просмотра к модели (к каждому «документу» в моем случае), но на данный момент я не мог понять, как изменить цвет для каждой конкретной ячейки просмотра, когда один прослушивается. (Мне нужно изменить только цвет фона текущей выбранной ячейки просмотра, чтобы все остальные ячейки просмотра имели цвет фона по умолчанию.)
<ListView x:Name = "DocumentListView"
ItemsSource = "{Binding GroupedDocuments}"
BackgroundColor = "WhiteSmoke"
HasUnevenRows = "True"
RefreshCommand = "{Binding LoadDocumentsCommand}"
IsPullToRefreshEnabled = "True"
Refreshing = "DocumentListView_OnRefreshing"
IsRefreshing = "{Binding IsBusy, Mode=OneWay}"
CachingStrategy = "RecycleElement"
IsGroupingEnabled = "True"
GroupDisplayBinding = "{Binding Key}"
GroupShortNameBinding = "{Binding Key}"
VerticalOptions = "StartAndExpand"
HorizontalOptions = "StartAndExpand"
Margin = "0, -20, 0, 0">
<ListView.GroupHeaderTemplate>
<DataTemplate>
<ViewCell Height = "25">
<Label x:Name = "DocumentDate"
FontSize = "Medium"
TextColor = "#2E588C"
VerticalOptions = "Center"
HorizontalTextAlignment = "Center"
Text = "{Binding Key}"/>
</ViewCell>
</DataTemplate>
</ListView.GroupHeaderTemplate>
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell Height = "155" Tapped = "DocumentViewCell_OnTapped">
<StackLayout Padding = "10, 5, 10, 5">
<Frame Padding = "0" HorizontalOptions = "FillAndExpand" HasShadow = "True">
<StackLayout Padding = "10" Orientation = "Horizontal" HorizontalOptions = "FillAndExpand" VerticalOptions = "StartAndExpand">
<StackLayout HorizontalOptions = "StartAndExpand">
<StackLayout Orientation = "Horizontal" Spacing = "15" Margin = "10, 10, 10, 0" HorizontalOptions = "StartAndExpand">
<Label Text = "{Binding Name}"
LineBreakMode = "NoWrap"
FontSize = "20"
TextColor = "CornflowerBlue"
FontAttributes = "Bold"/>
</StackLayout>
<StackLayout Orientation = "Horizontal" Spacing = "5" Margin = "12, 0, 0, 0" HorizontalOptions = "StartAndExpand">
<Label Text = "{Binding DocumentType.Name, StringFormat='Typ: {0}'}"
LineBreakMode = "NoWrap"
FontSize = "16"
TextColor = "Black"/>
</StackLayout>
<StackLayout Orientation = "Horizontal" Spacing = "5" Margin = "12, 3, 0, 0" HorizontalOptions = "StartAndExpand">
<Label Text = "{Binding TotalValue, StringFormat='Gesamtwert: {0:F2} €'}"
LineBreakMode = "NoWrap"
FontSize = "16"
TextColor = "Black"/>
</StackLayout>
<StackLayout Spacing = "5" Margin = "12, 3, 0, 0" Orientation = "Horizontal" HorizontalOptions = "StartAndExpand" VerticalOptions = "Start">
<Label Text = "{Binding TagCollectionString, StringFormat='Tags: {0}'}"
LineBreakMode = "WordWrap"
FontSize = "14"
TextColor = "Black"
VerticalOptions = "CenterAndExpand"/>
</StackLayout>
</StackLayout>
<StackLayout HorizontalOptions = "EndAndExpand" VerticalOptions = "Start" Margin = "0, 25, 25, 0">
<ImageButton HeightRequest = "85" MinimumWidthRequest = "85" x:Name = "ButtonEditDocument" Source = "baseline_more_vert_black_48.png" Clicked = "ButtonEditDocument_OnClicked" Margin = "0, 0, 15, 0" BackgroundColor = "Transparent" WidthRequest = "25" />
</StackLayout>
</StackLayout>
</Frame>
</StackLayout>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
Я придумал, как использовать привязки для сохранения текущего выбранного фона каждой ячейки просмотра в моей модели. На данный момент я столкнулся с проблемой, что пользовательский интерфейс не обновляется должным образом, когда связанные значения изменились. Вот код, который я написал до сих пор, и то, как страница обновляется при нажатии на другую ячейку просмотра:
Класс модели документа:
public class Document
{
public bool HasDefaultColor { get; set; }
public string CurrentlySelectedColorFromHex
{
get => ColorConverter.GetHexString(CurrentlySelectedColor);
}
[NotMapped]
public Color CurrentlySelectedColor => HasDefaultColor ? DefaultColor : ActivatedColor;
private static readonly Color DefaultColor = Color.WhiteSmoke;
private static readonly Color ActivatedColor = Color.FromHex("#2E588C");
}
Функция OnTapped в коде программной части:
private void DocumentViewCell_OnTapped(object sender, EventArgs e)
{
var documents = documentRepository.GetAll();
foreach (var document in documents)
document.HasDefaultColor = true;
selectedDocument.HasDefaultColor = false;
unitOfWork.Complete();
UpdatePage();
}
В UpdatePage() я хочу правильно обновить список после изменения связанной коллекции:
viewModel.LoadDocuments();
DocumentListView.BeginRefresh();
Извините, если это вопрос новичка, но я еще не нашел на него ответа или не смог понять, как правильно обновить пользовательский интерфейс, чтобы цвет фона каждой ячейки просмотра правильно обновлялся. По крайней мере, ограниченные значения сохраняются корректно при каждом вызове OnTapped().
Привет, ребята, я пробовал несколько вещей и застрял с обновлением связанного свойства модели. Я также пробовал триггеры данных, но не смог правильно изменить эти триггеры данных, поэтому они не работали так, как я ожидал.
До сих пор я добавлял пользовательский преобразователь bool в Color для преобразования связанного свойства:
public class BoolToColorConverter : IValueConverter
{
private static readonly Color DefaultColor = Color.WhiteSmoke;
private static readonly Color ActivatedColor = Color.FromHex("#2E588C");
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is bool activated)
return activated ? ActivatedColor : DefaultColor;
return DefaultColor;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is Color color)
{
if (color == DefaultColor)
return true;
}
return false;
}
Преобразователь цвета возвращает правильные значения, но я не могу понять, как обновить свойство модели во время выполнения в методе OnTapped() каждой ячейки просмотра.
В настоящее время это мой метод OnTapped():
private void DocumentViewCell_OnTapped(object sender, EventArgs e)
{
// Determine which document was selected
if (sender.GetType() == typeof(ViewCell))
{
ViewCell selectedViewCell = (ViewCell)sender;
if (selectedViewCell.BindingContext != null && selectedViewCell.BindingContext.GetType() == typeof(Document))
{
Document document = (Document)selectedViewCell.BindingContext;
// Update backing field selectedDocument for correct bindings and to show correct detail page
if (document != null)
selectedDocument = document;
}
}
Спасибо за любую помощь заранее и спасибо всем, кто прокомментировал до сих пор.
Хорошо, я подумал, что это правильный подход, поскольку я несколько раз видел этот ответ в Интернете. Похоже, я не могу избежать написания собственного рендерера. Спасибо за Ваш ответ.
Я думаю, что вы можете/должны сделать это с привязкой данных... ViewCells иногда перерабатываются между элементами, поэтому их перечисление не кажется хорошей идеей.
Хорошо, спасибо за помощь. Я посмотрю, смогу ли я разработать решение с привязками данных.
Это довольно просто, если вы сошлетесь на свойство BackgroundColor
вашего дочернего макета со ссылкой на привязку данных. Если вы хотите, я могу привести вам пример, учитывая, что вы публикуете свой XAML
Спасибо за вашу помощь, я не мог понять, как позволить всем ячейкам просмотра иметь цвет по умолчанию, когда конкретная ячейка просмотра нажимается / выбирается. Я предоставил код xaml в своем вопросе.
К сожалению, у меня нет времени, чтобы дать расширенный ответ, так что это придется сделать. По сути, вы говорите, что цвет фона зависит от выбранного элемента. Так что для меня логичным выбором было бы просто привязать его к этому, а затем использовать преобразователь значений, чтобы преобразовать его в цвет фона. Вы можете привязать другие значения к преобразователю значений, поэтому, если вы объявите его и привяжете текущий элемент, цвет по умолчанию и выбранный цвет. Все, что ему нужно сделать, это вернуть соответствующий цвет в зависимости от того, является ли входное значение тем же объектом, что и связанный элемент.
В дополнение к комментарию выше, вот ссылка, которая должна предоставить вам информацию, необходимую для настройки преобразователя значений: docs.microsoft.com/en-us/xamarin/xamarin-forms/app-fundamentals/…
Спасибо, @Knoop, я мог понять, как использовать конвертер для правильной привязки цвета. Я все еще застрял в том, что не знаю, как правильно обновить свойство связанной модели во время выполнения (при нажатии на ячейку просмотра). (См. мой обновленный вопрос). Я посмотрю, смогу ли я найти решение и опубликую его как ответ. Спасибо всем за то, что направили меня в правильном направлении!
если вы хотите, чтобы ваш пользовательский интерфейс обновлялся при изменении значения привязки, вы должны позволить своей модели реализовать INotifyPropertyChanged
Спасибо, @LeoZhu-MSFT за вашу помощь. Я внедрил NotifyPropteryChanged в свою модель, но застрял с правильным обновлением связанной коллекции в ViewModel. Я мог понять, как решить мою проблему, и теперь получил желаемый результат. Я опубликовал результат как ответ на этот вопрос, даже если первоначальный вопрос был немного другим. Спасибо за вашу помощь. :)
(См. ответ, который дал @Knoop, чтобы сделать это правильно с правильными привязками и командами форм xamarin)
Поскольку я смог выполнить свою первоначальную цель, которая заключалась в программном изменении цвета фона каждой ячейки просмотра в коде позади, я опубликую свой результат в качестве ответа.
Сначала я узнал, что перебирать ячейки просмотра — плохая идея, потому что это противоречит шаблону проектирования. Поэтому я использовал привязки, а также пользовательский конвертер BoolToColor для динамического обновления цвета фона ячеек просмотра.
Это код, который я написал:
Класс модели (документ):
public class Document : BaseModel<int>, IDocument, INotifyPropertyChanged
{
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string propertyName = "")
{
var changed = PropertyChanged;
changed?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
#endregion // INotifyPropertyChanged
public bool HasDefaultColor
{
get => hasDefaultColor;
set
{
hasDefaultColor = value;
OnPropertyChanged();
}
}
}
Преобразователь
public class BoolToColorConverter : IValueConverter (see question for detailed code...)
Реализация конвертера в xaml:
<Frame BackgroundColor = "{Binding HasDefaultColor, Converter = {converters:BoolToColorConverter}}" Padding = "0" HorizontalOptions = "FillAndExpand" HasShadow = "True">
И, наконец (это та часть, с которой я застрял), правильное обновление коллекции в привязанной модели представления в методе OnTapped():
private void DocumentViewCell_OnTapped(object sender, EventArgs e)
{
try
{
// Determine which document was selected
if (sender.GetType() == typeof(ViewCell))
{
ViewCell selectedViewCell = (ViewCell)sender;
if (selectedViewCell.BindingContext != null && selectedViewCell.BindingContext.GetType() == typeof(Document))
{
Document document = (Document)selectedViewCell.BindingContext;
if (document != null)
{
// Update default color (viewcell) for binded model
document.HasDefaultColor = !document.HasDefaultColor;
// Update backing field selectedDocument for correct bindings and to show correct detail page
ObservableCollection<Grouping<string, Document>> documents = viewModel.GroupedDocuments;
foreach (var group in documents)
{
foreach (var doc in group)
{
if (doc.Name == document.Name)
{
doc.HasDefaultColor = document.HasDefaultColor;
}
}
}
viewModel.GroupedDocuments = documents;
selectedDocument = document;
}
}
}
}
}
Наконец-то у меня появилось время, чтобы подготовить для вас ответ.
Я вижу, вы уже нашли обходной путь, но поскольку он не совсем соответствует философии дизайна Xamarin.Forms
(он выполняет жестко закодированную работу в коде программной части, чтобы получить то, что вы хотите, вместо использования Xamarin.Forms
функциональности.
В любом случае я разработаю 2 возможных решения, которые, на мой взгляд, лучше соответствуют хорошему Xamarin.Forms
дизайну.
Опция 1:
Этот параметр отслеживает текущий выбранный элемент и использует преобразователь значений для каждого элемента в списке, чтобы проверить, равен ли он этому выбранному элементу, и вернуть цвет на основе этого.
В нашем ViewModel
нам нужно настроить коллекцию для нашего ListView
нашего SelectedItem
свойства, которое уведомляет нас об изменениях свойств, и, наконец, ItemTappedCommand
, которое изменит наше SelectedItem
:
private ObservableCollection<ItemGroup> _itemGroups;
public ObservableCollection<ItemGroup> ItemGroups
{
get => _itemGroups;
set => SetProperty(ref _itemGroups, value);
}
private Item _selectedItem;
public Item SelectedItem
{
get => _selectedItem;
set => SetProperty(ref _selectedItem, value);
}
private ICommand _itemTappedCommand;
public ICommand ItemTappedCommand => _itemTappedCommand ?? (_itemTappedCommand = new Command<Item>((item) =>
{
SelectedItem = item;
}));
Затем нам понадобится ValueConverter
, который проверит равенство и вернет правильное Color
:
public class EqualityToColorConverter : IValueConverter
{
public Color EqualityColor { get; set; }
public Color InequalityColor { get; set; }
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value == null || parameter == null)
return InequalityColor;
if (parameter is Binding binding && binding.Source is View view)
{
parameter = view.BindingContext;
}
return value == parameter ? EqualityColor : InequalityColor;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
Этот конвертер использует небольшой обходной путь для извлечения фактического элемента из parameter
, поскольку по какой-то причине он продолжал возвращать мне Binding
вместо фактического Item
.
Теперь, когда у нас есть все необходимое, мы можем создать нашу страницу:
<ContentPage xmlns = "http://xamarin.com/schemas/2014/forms"
xmlns:x = "http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:converters = "clr-namespace:ColorChangeDemo.Converters"
x:Class = "ColorChangeDemo.Views.SelectedItemPage">
<ContentPage.Resources>
<ResourceDictionary>
<converters:EqualityToColorConverter x:Key = "equalityToColorConverter" EqualityColor = "Green" InequalityColor = "Gray" />
</ResourceDictionary>
</ContentPage.Resources>
<ContentPage.Content>
<ListView x:Name = "ListView" ItemsSource = "{Binding ItemGroups}" GroupShortNameBinding = "{Binding Key}" GroupDisplayBinding = "{Binding Key}" IsGroupingEnabled = "True">
<ListView.GroupHeaderTemplate>
<DataTemplate>
<ViewCell>
<Label Text = "{Binding Key}" />
</ViewCell>
</DataTemplate>
</ListView.GroupHeaderTemplate>
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<Button
x:Name = "Button"
Text = "{Binding Id}"
BackgroundColor = "{Binding Source = {x:Reference ListView}, Path=BindingContext.SelectedItem, Converter = {StaticResource equalityToColorConverter}, ConverterParameter = {Binding Source = {x:Reference Button}}}"
Command = "{Binding Source = {x:Reference ListView}, Path=BindingContext.ItemTappedCommand}"
CommandParameter = "{Binding .}"/>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</ContentPage.Content>
</ContentPage>
Преимущество этого решения заключается в том, что вам не нужно менять свой класс Item
(если у вас нет никакого контроля над ним), однако недостатком является то, что при каждом изменении выбора все элементы будут реагировать на измененное значение. Поэтому, если отображается много элементов, это может быть неоптимальным.
Вариант 2:
Эта опция добавит свойство Selected
к классу Item
и будет отслеживать ранее выбранный элемент, чтобы иметь возможность отменить его выбор при выборе другого элемента.
Мы снова начинаем с нашего ViewModel
, сначала с нашими свойствами:
private ObservableCollection<SelectableItemGroup> _selectableItemGroups;
public ObservableCollection<SelectableItemGroup> SelectableItemGroups
{
get => _selectableItemGroups;
set => SetProperty(ref _selectableItemGroups, value);
}
public ICommand ItemTappedCommand { get; }
И в конструкторе создаем нашу команду. Чтобы это работало, мы создаем локальную переменную, которую мы можем использовать для захвата в команде, чтобы мы могли отслеживать ранее выбранный элемент:
SelectableItem previous = null;
ItemTappedCommand = new Command<SelectableItem>((item) =>
{
if (previous != null)
previous.Selected = false;
previous = item;
item.Selected = true;
});
Теперь нам нужен ValueConverter
, который может преобразовать наше свойство Selected
в правильное Color
:
public class BoolToColorConverter: IValueConverter
{
public Color TrueColor { get; set; }
public Color FalseColor { get; set; }
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value != null && value is bool boolValue)
return boolValue ? TrueColor : FalseColor;
return FalseColor;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
И снова у нас есть все настройки для создания нашей страницы:
<ContentPage xmlns = "http://xamarin.com/schemas/2014/forms"
xmlns:x = "http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:converters = "clr-namespace:ColorChangeDemo.Converters"
x:Class = "ColorChangeDemo.Views.DeselectPage">
<ContentPage.Resources>
<ResourceDictionary>
<converters:BoolToColorConverter x:Key = "boolToColorConverter" TrueColor = "Green" FalseColor = "Gray" />
</ResourceDictionary>
</ContentPage.Resources>
<ContentPage.Content>
<ListView x:Name = "ListView" ItemsSource = "{Binding SelectableItemGroups}" GroupShortNameBinding = "{Binding Key}" GroupDisplayBinding = "{Binding Key}" IsGroupingEnabled = "True">
<ListView.GroupHeaderTemplate>
<DataTemplate>
<ViewCell>
<Label Text = "{Binding Key}" />
</ViewCell>
</DataTemplate>
</ListView.GroupHeaderTemplate>
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<Button
x:Name = "Button"
Text = "{Binding Id}"
BackgroundColor = "{Binding Selected, Converter = {StaticResource boolToColorConverter}}"
Command = "{Binding Source = {x:Reference ListView}, Path=BindingContext.ItemTappedCommand}"
CommandParameter = "{Binding .}"/>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</ContentPage.Content>
</ContentPage>
Преимущество этой опции в том, что она не просматривает весь список, чтобы убедиться, что все правильно Color
.
Демонстрационный проект
Чтобы увидеть это в действии, я загрузил небольшой демонстрационный проект, в котором я реализовал оба решения:
https://github.com/nknoop/ChangeColorDemo
Эй, @Knoop, спасибо за ваш подробный ответ и предоставленный демонстрационный проект :) Решение, которое я дал в качестве ответа, определенно не похоже на то, чтобы делать это в форме xamarin. Я реализовал второй вариант, который вы указали в своем ответе, его было легко реализовать, и он работает намного лучше, чем то, как я поступал (с обновлением всей коллекции каждый раз). Я очень ценю, что вы приложили столько усилий, чтобы ответить на мой вопрос. Наконец, я смог получить желаемый результат правильным способом (формы xamarin).
Привет, @Knoop, я обновил твой ответ как правильный ответ на этот вопрос. Спасибо за ваши усилия :)
Рад слышать, что у вас все получилось! Удачи тебе с твоим проектом:-)
Ты действительно, действительно не должен этого делать. Я не уверен, что кто-то может ответить на ваш вопрос, поскольку он является законным и может иметь решение, но путь, который вы выберете, в лучшем случае будет иметь много проблем.