Short version:
I am having a problem with the two-way binding of the IsSelected
property of the ListBox container and the ListBox item, which causes unexpected behaviour in the appearance of datatemplated items, when changing their IsSelected
property of items in my ViewModel. I am looking for help, since I don't understand what the problem is.
Long version: I am creating a CustomControl using a ListBox. I am using a DataTemplate to style the objects in the 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>
As you can see above, I am using the IsSelected
property to change the color of the stroke from black to white, when an item IsSelected
is true
. To select an item and correspondingly change its appearance I am binding the IsSelected
property in the ItemContainerStyle
to the IsSelected
property of my datatemplated items.
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>
I am now trying to implement double-click behaviour to select groups of identical items. I have this double-click event method in my code behind:
void listBoxItem_DoubleClick(object sender, MouseButtonEventArgs e)
{
(((ListBoxItem)sender).Content as Target).MouseSelectGroupCommand.Execute(null);
}
The command MouseSelectGroupCommand
of Target
finds the other Targets
of the group in the ObservableCollection TargetCollection
, which are identical to the selected one and sets their IsSelected
property to true
.
The problem I am now having, is that when I perform a double-click on a target, only that target changes its stroke color, but not the other targets of the group.
To try and debug I have done the following:
1) Confirm that the IsSelected
property of all targets in the group are indeed set to true
, which is the case.
2) I have changed the binding from <Setter Property="IsSelected" Value="{Binding Path=IsSelected, Mode=TwoWay}"/>
to <Setter Property="IsSelected" Value="{Binding Path=IsSelected, Mode=OneWayToSource}/>"
in <ListBox.ItemContainerStyle>
. When I do this, it works and the stroke color changes for the whole group as expected. However I lose the selection behaviour of the ListBox, which I would then have to reimplement (such as deselection, when selecting another item, etc.). I would therefore like to avoid this.
Furthermore I am using precisely the same method to change the DropShadowEffect
of the whole group, when a member-target of that group is being hovered (see DataTemplate) and in that case it works perfectly fine.
I am therefore left to conclude, that it somehow has to do with the binding of the IsSelected
property. I would appreciate any suggestions on how to resolve this.
Update:
Here is the code that is executed by the MouseSelectGroupCommand
. It sends a Message using MvvmLight Messenger to its containing collection, which the finds other targets that are identical and sets their IsSelected
property to true
. I know it is not pretty at all, but I am still very new to WPF it is what I have got working ATM. I would love to hear suggestions on how to handle this better, although that would be different question altogether.
MouseSelectGroupCommand executed on double-click:
public RelayCommand MouseSelectGroupCommand { get; set; }
private void ExecuteSelectTargetGroup()
{
List<Target> selectedTarget = new List<Target>();
selectedTarget.Add(this);
Messenger.Default.Send(new SelectTargetGroup(selectedTarget));
}
SelectGroup command, executed in the ObservableCollection containing the targets, when receiving the SelectTargetGroup
message:
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;
}
}
And this is how I have the command set up in the constructor of the ObservableCollection:
Messenger.Default.Register<SelectTargetGroup>(this, msg => SelectGroup(msg.SelectedTargets));
Update: It has become clear to me that the root of the problem is in my sloppy implementation. The answer Василий Шапенко should help me achieve a much cleaner implementation and therefore work around the problem, which is why I accepted it.