如何使用 Xamarin 表单中的 MVVM 仅为集合视图中的选定框架设置颜色?

How to set color only for the selected frame in collection view using MVVM in Xamarin forms?

我正在使用 RelativeSource 绑定 为集合视图中的框架绑定 背景颜色。但是集合视图中所有帧 的 背景颜色都在变化。我需要 仅为我选择的框架设置背景颜色

这是我的xaml代码

<StackLayout Padding="10">
    <CollectionView x:Name="list" ItemsSource="{Binding samplelist}">
        <CollectionView.ItemsLayout>
            <GridItemsLayout Orientation="Vertical" Span="2" HorizontalItemSpacing="10" VerticalItemSpacing="10" />
        </CollectionView.ItemsLayout>
        <CollectionView.ItemTemplate>
            <DataTemplate>
                <StackLayout>
                    <VisualStateManager.VisualStateGroups>
                        <VisualStateGroup Name="CommonStates">                        
                            <VisualState Name="Selected">
                                <VisualState.Setters>
                                    <Setter Property="BackgroundColor" Value="Green" />
                                </VisualState.Setters>
                            </VisualState>
                        </VisualStateGroup>
                    </VisualStateManager.VisualStateGroups>
                    <Frame  CornerRadius="10"  HasShadow="False" BackgroundColor="{Binding BackgroundTest,Mode=TwoWay, Converter={StaticResource colorConverter}}" HeightRequest="75" Margin="5,0,0,0" >
                        <StackLayout Orientation="Vertical">
                            <StackLayout.GestureRecognizers>
                                <TapGestureRecognizer Command="{Binding Source={x:Reference test}, Path=BindingContext.TriggerScene}"
                                                              CommandParameter="{Binding .}"/>
                            </StackLayout.GestureRecognizers>

这是我在 Viewmodel

中的代码
public bool FrameColorChange=true;
private Color _backgroundTest;
public Color BackgroundTest
{
    get { return _backgroundTest; }       
    set
    {
        if (value == _backgroundTest)
            return;
    
        _backgroundTest = value;
        OnPropertyChanged(nameof(BackgroundTest));
    }
}
private async void TriggerScene(Scene scene)
{

    if (FrameColorChange==true)
    {
        BackgroundTest = Color.Gray;
        FrameColorChange = false;
    }
    else
    {
        BackgroundTest = Color.White;
        FrameColorChange = true;
    }
}

我已经完成了一些修复,例如

但没有任何帮助。我也试过 SelectionChanged event.But SelectionChanged 的​​问题是它没有正确触发 因为有 TapGestureRecognizer 在我的相框里。我想要我的 TriggerScene 命令 中选定帧的颜色绑定 我的 viewmodel 中的 TapGestureRecognizer。我不想使用代码隐藏。我不知道如何解决这个问题,有什么建议吗?

您可以试试下面的代码。

Xaml:

     <StackLayout Padding="10">
        <CollectionView x:Name="list" ItemsSource="{Binding samplelist}"  SelectionMode="Single"  SelectionChanged="list_SelectionChanged" >
            <CollectionView.ItemsLayout>
                <GridItemsLayout Orientation="Vertical" Span="2" HorizontalItemSpacing="10" VerticalItemSpacing="10" />
            </CollectionView.ItemsLayout>
            <CollectionView.ItemTemplate>
                <DataTemplate>

                    <Frame  CornerRadius="10"  HasShadow="False" BackgroundColor="{Binding BackgroundTest}" HeightRequest="75" Margin="5,0,0,0" >
                        <StackLayout Orientation="Vertical">
                            <Label Text="{Binding str}"></Label>
                         
                        </StackLayout>
                    </Frame>

                </DataTemplate>
            </CollectionView.ItemTemplate>
        </CollectionView>
    </StackLayout>

后面的代码:

   public Page2()
    {
        InitializeComponent();
        this.BindingContext = new MyViewModel();
    }

    private void list_SelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        MyModel previous = e.PreviousSelection.FirstOrDefault() as MyModel;
        MyModel current = e.CurrentSelection.FirstOrDefault() as MyModel;

        //Set the current to the color you want
        current.BackgroundTest = "Red";


        if (previous != null)
        {
            //Reset the previous to defaulr color
            previous.BackgroundTest = "Gray";
        }  
    }

视图模型:

public class MyViewModel
{
    public ObservableCollection<MyModel> samplelist { get; set; }
    public MyViewModel()
    {
        samplelist = new ObservableCollection<MyModel>()
        {
            new MyModel(){ BackgroundTest="Gray", str="hello1"},
            new MyModel(){ BackgroundTest="Gray", str="hello2"},
            new MyModel(){ BackgroundTest="Gray", str="hello3"},
            new MyModel(){ BackgroundTest="Gray", str="hello4"},
            new MyModel(){ BackgroundTest="Gray", str="hello5"},
            new MyModel(){ BackgroundTest="Gray", str="hello6"},
            new MyModel(){ BackgroundTest="Gray", str="hello7"},
            new MyModel(){ BackgroundTest="Gray", str="hello8"},
        };
    }
}

型号:

public class MyModel : INotifyPropertyChanged
{

    public string str { get; set; }
    private string _backgroundTest;
    public string BackgroundTest
    {
        get { return _backgroundTest; }
        set
        {
            _backgroundTest = value;
            OnPropertyChanged("BackgroundTest");
        }
    }

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

更新:

如果您在 DataTemplate 中有 TapGestureRecognizer,您可以使用 VisualState 而不是 CollectionView 的 SelectionChanged

   <ContentPage.Resources>
    <ResourceDictionary>
        <Style TargetType="StackLayout">
            <Setter Property="VisualStateManager.VisualStateGroups">
                <VisualStateGroupList>
                    <VisualStateGroup>
                        <VisualState x:Name="Selected">
                            <VisualState.Setters>
                                <Setter Property="BackgroundColor" Value="Accent" />
                            </VisualState.Setters>
                        </VisualState>
                        <VisualState x:Name="UnSelected">
                            <VisualState.Setters>
                                <Setter Property="BackgroundColor" Value="Blue" />
                            </VisualState.Setters>
                        </VisualState>
                    </VisualStateGroup>
                </VisualStateGroupList>
            </Setter>
        </Style>
    </ResourceDictionary>
</ContentPage.Resources>

Xaml:

   <StackLayout.GestureRecognizers>
                                <TapGestureRecognizer Tapped="TapGestureRecognizer_Tapped"></TapGestureRecognizer>
                            </StackLayout.GestureRecognizers>

后面的代码:

    StackLayout lastElementSelected;
    private void TapGestureRecognizer_Tapped(object sender, EventArgs e)
    {

        if (lastElementSelected != null)
            VisualStateManager.GoToState(lastElementSelected, "UnSelected");

        VisualStateManager.GoToState((StackLayout)sender, "Selected");

        lastElementSelected = (StackLayout)sender;

    }

可能有很多方法可以解决你的问题,我不会声称找到了最好的方法,但它仍然是一种(简单的)方法。

我会在下面为您添加一个complete-working-minimal-sample,这正是您想要的,所以请随意copy-paste并根据您的需要进行调整。


实现目标的一种方法是:

  1. Object 中添加一个名为 Selected 或类似名称的 属性 来填充 Collection (samplelist) 绑定到您的 CollectionView.
  2. FrameBackgroundColor 属性 绑定到 Selected 属性 并设置为它是一个 转换器,从 布尔值 值(已选择?)变为 颜色(选择颜色).
  3. 然后当 collection 中的项目被点击并且 TapGestureRecognizer 触发时,您可以将所选项目作为 CommandParameter命令
  4. 在Command-handler中设置传递项的Selected属性为true
  5. Selected属性变化时,调用ConverterBackgroundColor属性 已更新。

以下示例说明了这一点:

Page1.xaml

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="App1.Page1"
             x:Name="test"
             xmlns:local="clr-namespace:App1">
    
    <ContentPage.BindingContext>
        <local:ViewModel/>
    </ContentPage.BindingContext>

    <ContentPage.Resources>
        <ResourceDictionary>
            <local:SelectedToColorConverter x:Key="selectedToColorConverter"/>
        </ResourceDictionary>
    </ContentPage.Resources>
    
    <ContentPage.Content>
        <StackLayout Padding="10">
            <CollectionView ItemsSource="{Binding samplelist}">
                
                <CollectionView.ItemsLayout>
                    <GridItemsLayout Orientation="Vertical" Span="2" HorizontalItemSpacing="10" VerticalItemSpacing="10" />
                </CollectionView.ItemsLayout>
                
                <CollectionView.ItemTemplate>
                    <DataTemplate>
                        <StackLayout>

                            <Frame  CornerRadius="10"  
                                    HasShadow="False" 
                                    BackgroundColor="{Binding Selected, Converter={x:StaticResource selectedToColorConverter}}" 
                                    HeightRequest="75" 
                                    Margin="5,0,0,0" >
                                <StackLayout Orientation="Vertical">
                                    <StackLayout.GestureRecognizers>
                                        <TapGestureRecognizer Command="{Binding Source={x:Reference test}, Path=BindingContext.TriggerSceneCommand}" CommandParameter="{Binding .}"/>
                                    </StackLayout.GestureRecognizers>
                                    <Label Text="{Binding Text}"/>
                                    <Label Text="{Binding Description}"/>
                                </StackLayout>
                            </Frame>
                            
                        </StackLayout>
                    </DataTemplate>
                </CollectionView.ItemTemplate>
            </CollectionView>
        </StackLayout>

    </ContentPage.Content>
</ContentPage>

ViewModel.cs

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using System.Runtime.CompilerServices;
using Xamarin.Forms;

namespace App1
{
    public class ViewModel
    {

        public ViewModel()
        {
            samplelist = new List<item> 
            { 
                new item { Text = "Uno", Description = "Uno Description bla bla" },
                new item { Text = "Dos", Description = "Dos Description bla bla" },
                new item { Text = "Tres", Description = "Tres Description bla bla" }
            };

            TriggerSceneCommand = new Command<object>(TriggerScene);
        }

        public List<item> samplelist { get; set; }


        public Boolean isMultiSelect = false;
        

        public Command TriggerSceneCommand { get; set; }
        private void TriggerScene(object selectedItem)
        {
            ((item)selectedItem).Selected = !((item)selectedItem).Selected;

            if (!isMultiSelect)
            {
                foreach (item otherItem in samplelist)
                {
                    if (otherItem != selectedItem)
                    {
                        otherItem.Selected = false;
                    }
                }
            }
        }

    }

    public class item : INotifyPropertyChanged
    {

        public Boolean _selected;
        public Boolean Selected 
        {
            get 
            {
                return _selected;
            }
            set
            {
                _selected = value;
                OnPropertyChanged();
            }
        }

        public String Text { get; set; }

        public String Description { get; set; }

        public event PropertyChangedEventHandler PropertyChanged;
        public void OnPropertyChanged([CallerMemberName] string name = "")
        {
            var propertyChanged = PropertyChanged;

            propertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
        }

    }


    public class SelectedToColorConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            return ((Boolean)value) ? Color.Gray : Color.White;
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }
}

请注意,作为 奖金 有一个名为 isMultiSelect 的 属性,如果为真则允许多个项目to be marked/colored and if false, then when one item is selected all the others get their Selected 属性 set to false.

我已经在另一个答案中发布了一种解决问题的方法,现在我想提供一个更简单的解决方案(尽管我不会声称它是最好的)。

注意 在此解决方案中,与我的其他答案相反,您不需要向您的对象添加多余的 属性集合视图,但是新的 属性 是直接在 ViewModel 中定义的。


解决您的问题的一种方法是:

  1. 在您的 ViewModel 中定义一个名为 SelectedItem 的 属性:这将跟踪当前选定的项目。
  2. 然后您将 FrameBackgroundColor 绑定到新的 属性: SelectedItem 为此您需要一个 ValueConverter 接受 SelectedItemConverterParameter:当前 Frame.
  3. Frame 中,在 StackLayout 中你有 good old TapGestureRecognizer 其处理程序在调用时将设置 Selected Item.
  4. 设置 SelectedItem 后,将调用 On属性Changed 并为 CollectionView 中的每个项目调用 ValueConverter。然后,转换器检查框架的 BindingContext(它绑定到的项目!)是否与 SelectedItem 相同,如果是,则将其颜色设置为灰色(选中!)

当然,下面我添加了一个完整的最小工作样本。随意复制粘贴并使用它。

Page1.xaml

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="App1.Page1"
             x:Name="test"
             xmlns:local="clr-namespace:App1">
    
    <ContentPage.BindingContext>
        <local:ViewModel/>
    </ContentPage.BindingContext>

    <ContentPage.Resources>
        <ResourceDictionary>
            <local:SelectedToColorConverter x:Key="selectedToColorConverter"/>
        </ResourceDictionary>
    </ContentPage.Resources>
    
    <ContentPage.Content>
        <StackLayout Padding="10">
            <CollectionView ItemsSource="{Binding samplelist}">
                
                <CollectionView.ItemsLayout>
                    <GridItemsLayout Orientation="Vertical" Span="2" HorizontalItemSpacing="10" VerticalItemSpacing="10" />
                </CollectionView.ItemsLayout>
                
                <CollectionView.ItemTemplate>
                    <DataTemplate>
                        <StackLayout>

                            <Frame x:Name="frame" CornerRadius="10"  
                                    HasShadow="False" 
                                    BackgroundColor="{Binding Source={x:Reference test}, Path=BindingContext.SelectedItem, Converter={x:StaticResource selectedToColorConverter}, ConverterParameter={x:Reference frame}}" 
                                    HeightRequest="75" 
                                    Margin="5,0,0,0" >
                                <StackLayout Orientation="Vertical">
                                    <StackLayout.GestureRecognizers>
                                        <TapGestureRecognizer Command="{Binding Source={x:Reference test}, Path=BindingContext.TriggerSceneCommand}" CommandParameter="{Binding .}"/>
                                    </StackLayout.GestureRecognizers>
                                    <Label Text="{Binding Text}"/>
                                    <Label Text="{Binding Description}"/>
                                </StackLayout>
                            </Frame>
                            
                        </StackLayout>
                    </DataTemplate>
                </CollectionView.ItemTemplate>
            </CollectionView>
        </StackLayout>

    </ContentPage.Content>
</ContentPage>

ViewModel.cs

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using System.Runtime.CompilerServices;
using Xamarin.Forms;

namespace App1
{
    public class ViewModel : INotifyPropertyChanged
    {

        public ViewModel()
        {
            samplelist = new List<item> 
            { 
                new item { Text = "Uno", Description = "Uno Description bla bla" },
                new item { Text = "Dos", Description = "Dos Description bla bla" },
                new item { Text = "Tres", Description = "Tres Description bla bla" }
            };

            TriggerSceneCommand = new Command<item>(TriggerScene);
        }

        public List<item> samplelist { get; set; }


        private item _selectedItem;
        public item SelectedItem
        {
            get => _selectedItem;
            set
            {
                _selectedItem = value;
                OnPropertyChanged();
            }
        }



        public Command TriggerSceneCommand { get; set; }
        private void TriggerScene(item newSelectedItem)
        {
           SelectedItem = newSelectedItem;
        }


        public event PropertyChangedEventHandler PropertyChanged;
        public void OnPropertyChanged([CallerMemberName] string name = "")
        {
            var propertyChanged = PropertyChanged;

            propertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
        }

    }

    public class item
    {

        public String Text { get; set; }

        public String Description { get; set; }

    }


    public class SelectedToColorConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {

            Color result = Color.White;


            if (value != null && parameter != null && ((Frame)parameter).BindingContext != null && (item)value == (item)((Frame)parameter).BindingContext)
            {
                result = Color.Gray;
            }


            return result;

        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            return null;
        }
    }
}

为什么不呢,让我们在这里放一个奖金

您可以在 TapGestureRecognizer 的事件处理程序中添加两行代码,以 return 在一段时间(也许是三秒?)后恢复原来的颜色。

只需更改 ViewModel 中的 TriggerScene 方法如下(参见代码注释):

private void TriggerScene(item newSelectedItem)
{
    // Highlight selection!
    SelectedItem = newSelectedItem;

    // Sit and wait...
    await Task.Delay(3000);

    // Go back to normal!
    SelectedItem = null;
}