ListBoxItem和ListBoxContainer之间的IsSelected属性的双向绑定会导致意外行为。 OneWayToSource有效。为什么呢?

时间:2016-02-17 12:15:23

标签: c# wpf xaml listbox

简短版本: 我遇到了ListBox容器的IsSelected属性和ListBox项的双向绑定问题,这会在更改项目的IsSelected属性时导致datatemplated项的外观出现意外行为在我的ViewModel中。我正在寻求帮助,因为我不明白问题所在。

长版: 我正在使用ListBox创建CustomControl。我正在使用DataTemplate来设置ListBox中对象的样式。

的DataTemplate

    <DataTemplate DataType="{x:Type substratePresenter:Target}">
        <Ellipse Fill="{Binding MyColor}"
                 Width="{Binding Source={StaticResource WellSize}}"
                 Height="{Binding Source={StaticResource WellSize}}"
                 StrokeThickness="1.5"
                 Canvas.Left="{Binding Path=XPos}"
                 Canvas.Top="{Binding Path=YPos}"
                 ToolTip="{Binding Name}"
                 SnapsToDevicePixels="True"
                 Cursor="Hand">
                <i:Interaction.Triggers>
                    <i:EventTrigger EventName="MouseEnter">
                        <i:InvokeCommandAction Command="{Binding Path=MouseEnterCommand}"/>
                    </i:EventTrigger>
                    <i:EventTrigger EventName="MouseLeave">
                        <i:InvokeCommandAction Command="{Binding Path=MouseLeaveCommand}"/>
                    </i:EventTrigger>
                </i:Interaction.Triggers>
            <Ellipse.Effect>
                <!--THIS IS HACK SO THAT THE INITIAL STATE OF THE HOVEROVER SHADOW IS "OFF"--> 
                <DropShadowEffect Color="Blue" BlurRadius="10" ShadowDepth="0" Opacity="0" />
            </Ellipse.Effect>

            <Ellipse.Style>
                <Style TargetType="Ellipse">
                    <Style.Resources>
                        <!-- REF for using Storyboard animation, Glowon: http://stackoverflow.com/questions/1425380/how-to-animate-opacity-of-a-dropshadoweffect -->
                        <Storyboard x:Key="GlowOn">
                            <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetProperty="(Effect).Opacity">
                                <SplineDoubleKeyFrame KeyTime="0:0:0.0" Value="1"/>
                            </DoubleAnimationUsingKeyFrames>
                        </Storyboard>
                        <Storyboard x:Key="GlowOff">
                            <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetProperty="(Effect).Opacity">
                                <SplineDoubleKeyFrame KeyTime="0:0:0.0" Value="0"/>
                            </DoubleAnimationUsingKeyFrames>
                        </Storyboard>
                    </Style.Resources>

                    <Setter Property="Effect">
                        <Setter.Value>
                            <DropShadowEffect Color="Blue" BlurRadius="10" ShadowDepth="0" Opacity=".75" />
                        </Setter.Value>
                    </Setter>

                    <Setter Property="Stroke" Value="Black"/>
                    <Style.Triggers>
                        <!--Handel target target selection-->
                        <DataTrigger Binding="{Binding Path=IsSelected}" Value="True">
                                <Setter Property="Stroke" Value="White"/>
                        </DataTrigger>

                        <!--Handel target hovering-->
                        <!-- REF for using DataTrigger: https://msdn.microsoft.com/de-de/library/aa970679%28v=vs.90%29.aspx -->
                        <DataTrigger Binding="{Binding IsGroupHovered}" Value="True">
                            <DataTrigger.EnterActions>
                                <BeginStoryboard Storyboard="{StaticResource GlowOn}"/>
                            </DataTrigger.EnterActions>
                            <DataTrigger.ExitActions>
                                <BeginStoryboard Storyboard="{StaticResource GlowOff}"/>
                            </DataTrigger.ExitActions>
                        </DataTrigger>
                    </Style.Triggers>
                </Style>
            </Ellipse.Style>
        </Ellipse>
    </DataTemplate>

如上所示,当项IsSelectedIsSelected时,我使用true属性将笔画的颜色从黑色更改为白色。要选择一个项目并相应地更改其外观,我将IsSelected中的ItemContainerStyle属性绑定到我的datatemplated项目的IsSelected属性。

ListBox XAML

    <ListBox
        x:Name="TargetListBox"
        BorderThickness="0"
        Width="{StaticResource LayoutGridWidthColumn1}"
        Height="{StaticResource LayoutGridHeightRow1}"
        ItemsSource="{Binding Path=TargetCollection}"
        SelectionMode="Extended"
        Grid.Column="1" Grid.Row="1"
        Background="Transparent"
    >

        <i:Interaction.Behaviors>
            <behavior:RubberBandBehavior />
        </i:Interaction.Behaviors>

        <ListBox.ItemsPanel>
            <ItemsPanelTemplate>
                <Canvas IsItemsHost="True" Background="Transparent"/>
            </ItemsPanelTemplate>
        </ListBox.ItemsPanel>

        <ListBox.ItemContainerStyle>
            <Style TargetType="{x:Type ListBoxItem}">
                <EventSetter Event="MouseDoubleClick" Handler="listBoxItem_DoubleClick" />

                <Setter Property="Background" Value="Transparent"/>

                <Setter Property="Canvas.Left"
                        Value="{Binding XPos, Converter={StaticResource horizontalValueConverter},
                        ConverterParameter={StaticResource substrateWidth}}"/>

                <Setter Property="Canvas.Top"
                        Value="{Binding YPos, Converter={StaticResource verticalValueConverter},
                        ConverterParameter={StaticResource substrateHeight}}"/>

                <!--Bind IsSelected property of ListBoxItem to that of the Target-->
                <!--REF: http://stackoverflow.com/questions/1875450/binding-the-isselected-property-of-listboxitem-to-a-property-on-the-object-from-->

                <Setter Property="IsSelected" Value="{Binding Path=IsSelected, Mode=TwoWay}"/>
                <!--<Setter Property="IsSelected" Value="{Binding Path=IsSelected, Mode=OneWayToSource}"/>-->

                <!--Hide the background-highlighting of the ListBox-Selection, since we handle this from the Items-->
                <!--REF: http://stackoverflow.com/questions/2138200/change-background-color-for-selected-listbox-item-->
            </Style>
        </ListBox.ItemContainerStyle>
        <!--REF: http://stackoverflow.com/questions/4343793/how-to-disable-highlighting-on-listbox-but-keep-selection-->
        <ListBox.Resources>
            <SolidColorBrush x:Key="{x:Static SystemColors.HighlightBrushKey}" Color="Transparent" />
            <SolidColorBrush x:Key="{x:Static SystemColors.HighlightTextBrushKey}" Color="Black" />
            <SolidColorBrush x:Key="{x:Static SystemColors.ControlBrushKey}" Color="Transparent" />
        </ListBox.Resources>
    </ListBox>

我现在正尝试实施双击行为以选择相同项目的组。我的代码中有这个双击事件方法:

    void listBoxItem_DoubleClick(object sender, MouseButtonEventArgs e)
    {
        (((ListBoxItem)sender).Content as Target).MouseSelectGroupCommand.Execute(null);
    }

MouseSelectGroupCommand的{​​{1}}命令在ObservableCollection Target中找到该组的另一个Targets,它与所选的一组相同并设置其TargetCollection }属性IsSelected

我现在遇到的问题是,当我对目标执行双击时,只有该目标会更改其笔触颜色,而不会更改该组的其他目标。

要尝试调试,我已完成以下操作: 1)确认组中所有目标的true属性确实设置为IsSelected,这种情况。 2)我已将true中的约束从<Setter Property="IsSelected" Value="{Binding Path=IsSelected, Mode=TwoWay}"/>更改为<Setter Property="IsSelected" Value="{Binding Path=IsSelected, Mode=OneWayToSource}/>"。当我这样做时,它会工作,并且整个组的笔触颜色会按预期变化。但是我丢失了ListBox的选择行为,然后我必须重新实现(例如取消选择,选择另一个项目等)。因此,我想避免这种情况。

此外,我正在使用完全相同的方法来更改整个组的<ListBox.ItemContainerStyle>,当该组的成员目标正在悬停时(请参阅DataTemplate),在这种情况下,它完全正常。

因此,我得出结论,它与DropShadowEffect属性的绑定有某种关系。如果有任何解决方法,我将不胜感激。

更新

以下是IsSelected执行的代码。它使用MvvmLight Messenger将消息发送到其包含的集合,该集合查找其他相同的目标并将其MouseSelectGroupCommand属性设置为IsSelected。我知道它根本不漂亮,但我仍然是WPF的新手,这是我工作的ATM。我很想听听如何更好地处理这个问题的建议,尽管这完全是不同的问题。

双击时执行

MouseSelectGroupCommand

true

SelectGroup 命令,在收到public RelayCommand MouseSelectGroupCommand { get; set; } private void ExecuteSelectTargetGroup() { List<Target> selectedTarget = new List<Target>(); selectedTarget.Add(this); Messenger.Default.Send(new SelectTargetGroup(selectedTarget)); } 消息时,在包含目标的ObservableCollection中执行:

SelectTargetGroup

这就是我在ObservableCollection的构造函数中设置命令的方法:

public void SelectGroup(IList<Target> selectedTarget)
{
    IList<Target> targetGroup = GetTargetsWithSameActions(selectedTarget[0]);
    SetGroupSelected(targetGroup);
}
public void SetGroupSelected(IList<Target> targetGroup)
{
    foreach (Target target in targetGroup)
    {
        target.PropertyChanged -= TargetPropertyChanged;
        target.IsSelected = true;
        target.PropertyChanged += TargetPropertyChanged;
    }
}

更新:我很清楚问题的根源在于我的草率实施。答案ВасилийШапенко应该帮助我实现更清洁的实施,从而解决问题,这就是我接受它的原因。

1 个答案:

答案 0 :(得分:2)

好的,这是一个小解决方案:

首先是背景部分。在下面的代码中,我们创建一个主视图模型,向其添加属性项,并用一堆模型填充它。 OnModelSelectionChanged通过选择模型组来完成工作。

    public class MainViewModel
{
    private ObservableCollection<SelectionItemViewModel> items;

    public MainViewModel()
    {
        FillItems();
    }

    private void FillItems()
    {
        var models=Enumerable.Range(0, 10)
            .SelectMany(
                index =>
                    Enumerable.Range(0, 3)
                        .Select(i => new Model() {Id = index, Name = string.Format("Name_{0}_{1}", index, i)})).Select(
                            delegate(Model m)
                            {
                                var selectionItemViewModel = new SelectionItemViewModel()
                                {
                                    Value = m
                                };
                                selectionItemViewModel.PropertyChanged += OnModelSelectionChanged;
                                return selectionItemViewModel;
                            });

        Items=new ObservableCollection<SelectionItemViewModel>(models);
    }

    private void OnModelSelectionChanged(object sender, PropertyChangedEventArgs e)
    {
        if (e.PropertyName == "IsSelected")
        {
            var model = sender as SelectionItemViewModel;
            foreach (var m in Items.Where(i=>i.Value.Id==model.Value.Id && model!=i))
            {
                if (m.IsSelected != model.IsSelected)// This one to prevent infinite loop on selection, on double click there is no need for it
                {
                    m.IsSelected = model.IsSelected;
                }
            }
        }
    }

    public ObservableCollection<SelectionItemViewModel> Items

    {
        get { return items; }
        set { items = value; }
    }
}

public class SelectionItemViewModel:INotifyPropertyChanged
{
    private bool isSelected;

    public event PropertyChangedEventHandler PropertyChanged;

    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        var handler = PropertyChanged;
        if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
    }

    public bool IsSelected
    {
        get { return isSelected; }
        set { isSelected = value;
            OnPropertyChanged();//For .Net<4.5, use OnPropertyChanged("IsSelected")
        }
    }

    public  Model Value { get; set; }
}

public class Model
{
    public int Id { get; set; }
    public string Name { get; set; }
}

XAML。这是简单的绑定,没有什么复杂的。

 <Window x:Class="WpfApplication1.MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            Title="MainWindow" Height="350" Width="525">
        <Grid>
            <ListBox SelectionMode="Multiple" ItemsSource="{Binding Items}" DisplayMemberPath="Value.Name">
             <ListBox.ItemContainerStyle>
                 <Style TargetType="ListBoxItem">
                     <Setter Property="IsSelected" Value="{Binding IsSelected}"/>
                 </Style>
             </ListBox.ItemContainerStyle>
            </ListBox>
        </Grid>
    </Window>

MainWindow.xaml.cs - 这里我们将ViewModel放入MainWindow DataContext:

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        DataContext = new MainViewModel();
    }

}

双击支持:

在MainWindow.xaml.cs中:

private void Control_OnMouseDoubleClick(object sender, MouseButtonEventArgs e)
        {

            var source = e.OriginalSource as FrameworkElement;
            var mainViewModel = DataContext as MainViewModel;
            if (source != null)
            {
                var model = source.DataContext as SelectionItemViewModel;
                model.IsSelected = !model.IsSelected;
                if (model != null)
                {
                    foreach (var m in mainViewModel.Items.Where(i => i.Value.Id == model.Value.Id && model != i))
                    {
                        if (m.IsSelected != model.IsSelected)
                        {
                            m.IsSelected = model.IsSelected;
                        }
                    }
                }
            }
        }

在MainWindow.xaml:

 <ListBox MouseDoubleClick="Control_OnMouseDoubleClick" SelectionMode="Multiple" ItemsSource="{Binding Items}" DisplayMemberPath="Value.Name">

并在OnModelSelection中注释代码。

这是一种直接的粗略方法。更优雅的方法是创建绑定到双击并附加到ListBoxItem的命令,但这需要更多代码来编写和理解附加属性的概念。

另请参阅MouseButtonEventArgs,它将帮助您确定单击哪个按钮以及按下了哪个控制键。

进一步阅读的关键词:InputBinding,AttachedProperty,ICommand。