0

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.

halfer
  • 19,824
  • 17
  • 99
  • 186
packoman
  • 1,230
  • 1
  • 16
  • 36
  • Have you tried to take a look at ListBox.SelectionChanged event? Could you please provide your MouseSelectGroupCommand code? And as suggestion, it would be better set IsSelected on ViewModel rather than ListBoxItem when executiong Command. – Василий Шапенко Feb 17 '16 at 12:34
  • @ВасилийШапенко Thanks for your comments. I updated the question as you requested. I did not look at ListBox.SelectionChanged. I am still quite new to WPF and figuring things out. Could you perhaps elaborate on what you mean by "it would be better set IsSelected on ViewModel rather than ListBoxItem when executiong Command"? Thanks! – packoman Feb 17 '16 at 13:01
  • I mean that it would be simpler and better operate underlying objects in terms of selection/unselection(i.e. set ViewModel.IsSelected property to needed state). BTW, looks like you have very strange lines of code: target.PropertyChanged -= TargetPropertyChanged; target.IsSelected = true; target.PropertyChanged += TargetPropertyChanged; Maybe you should only target.IsSelected = true; – Василий Шапенко Feb 17 '16 at 13:29
  • So would that mean, that I sould have a ViewModel for each target (i.e. a List of ViewModels), that I then bind to the ListBox? Regarding the strange lines of code: Those lines are necessary as a work-around for some other logic, which I am using to trigger code, when properties of the targets change. Also not pretty (I know), but from what I can tell this is not the cause of the error, since when calling directly it works as expected. – packoman Feb 17 '16 at 14:25
  • Correct me if i wrong.As i understood you have a list of items,and you want to select items similar by some kind.The way i would solve this problem could be like that: 1. Create a ViewModel for ListBox, wich wraps my Target object, and has IsSelected property. 2.Create a logic on backend which selects all similar models by selecting one and unselects non-similar(if needed). 3. Do the required bindings on UI(as you do currently, two-way binding) My thoughts are abstract, and based of my understanding of what kind of problem you are trying to solve. – Василий Шапенко Feb 18 '16 at 08:39
  • @ВасилийШапенко You have understood correctly, what I am trying to do. Unfortunately I don't understand your comment 100%. What would I gain over binding my List of models via ItemSource as I am doing right now? I am absolutely sure that your proposed solution would be much better, but I don't know how to implement it. As you can tell from the convoluted mess, that is my code, I am still fighting with the concept of ViewModels and WPF. If it's not too much trouble, could you perhaps whip up a short example and post it as an answer? – packoman Feb 18 '16 at 09:20
  • @ВасилийШапенко I would gladly accept it as the answer, since I am now sure, that my problem is not with the two-way binding, but with my horrible implementation. I would revise my question accordingly. I originally intended to use as much of the ListBox behaviour as possible, but what got me in trouble, is that I need to implement two different selection behaviours depending on SingleClick and DoubleClick, i.e. SingleClick: Only the selected item; DoubleClick: Selection of the group of similar items. And that really messed up my plans. – packoman Feb 18 '16 at 09:25
  • Ok, i am writing simple solution right now as a proof of concept – Василий Шапенко Feb 18 '16 at 09:36
  • I have posted a small solution, feel free to ask if something needs more explanation – Василий Шапенко Feb 18 '16 at 09:48

1 Answers1

2

Ok, here is a small solution:

First is background part. In the code below we create a main view model, add property Items to it, and fill it with bunch of models. OnModelSelectionChanged does the work by selecting model groups.

    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. Here is simple binding, nothing complex.

 <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 - here we put our ViewModel into MainWindow DataContext:

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

}

For double click support:

In 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;
                        }
                    }
                }
            }
        }

In MainWindow.xaml:

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

And comment the code inside OnModelSelection.

That is a direct rough approach. More elegant way is to create command binded to double click and attached to ListBoxItem, but this requires more code to write and understanding the concept of Attached Properties.

Also take a look at MouseButtonEventArgs, which will help you to determine which button is clicked, and which control key is pressed.

Key words for further readings: InputBinding,AttachedProperty,ICommand.

  • This is awesome! Thank you so much for the example! It hadn't occured to me yet to just instantiated a list of ViewModels, which wrap around the models and are the ItemSource. With all my ugly coding, I was wondering how I was supposed to separate the model from the ViewModel in my ListBox. I am working on something right now, but will accept this as soon, as I tested and understood it. One more thing though: Is it possible to distinguish between SingleClick and DoubleClick in OnModelSelectionChanged, and handle the selection behaviour accordingly (see my last comment above)? Thanks again! – packoman Feb 18 '16 at 10:18
  • 1
    I didn't get around to testing it yet, but as far as I can see, I understand the code and it looks good, so I accepted it. I will comment again if any question comes up. I hope that's OK. Thanks you for your amazing help. This was really awesome! People like you make StackOverflow great! – packoman Feb 18 '16 at 13:04
  • Ok. So I just test your example and got it to work after a few modifications. E.g. I had to change `OnPropertyChanged();` to `OnPropertyChanged("IsSelected");`. Is this because I am using VS2010 and `CallerMemberName` does not work correctly? I installed the Microsoft.Bcl NuGet package, which according to this http://stackoverflow.com/questions/13381917/is-the-callermembername-attribute-in-4-5-able-to-be-faked should provide it in .net4.0. Also I needed to comment the line `model.IsSelected = !model.IsSelected;` in `Control_OnMouseDoubleClick`, which I don't really understand. – packoman Feb 19 '16 at 09:43
  • Furthermore I had to change `SelectionMode` to `Extended` to get the desired ListBox behavior (i.e. deselection of the previous item when selecting a new item). This is really great! I hope it will not be too much work to integrate it into my project. Regarding my changes: Not sure if they are errors on my end. If not perhaps you could update the question. – packoman Feb 19 '16 at 09:45
  • @packoman OnPropertyChanged() introduced in .net 4.5, you are right, it should not work at 4.0, model.IsSelected=!model.IsSelected was introduced to prevent infinite loop on selection event. In case of Double click you dont need it. – Василий Шапенко Feb 19 '16 at 09:46
  • Ok. I see. Not sure, why I had to uncomment the line `model.IsSelected=!model.IsSelected` to get it to work then. But with it, the code never enters the `if (m.IsSelected != model.IsSelected)` statement. – packoman Feb 19 '16 at 09:50
  • @packoman model.IsSelected = !model.IsSelected; is needed line, it gives a "checkbox-like" behavior on double-click. – Василий Шапенко Feb 19 '16 at 09:52
  • Ok. I figured it out: Commenting that line is needed to get group selection to work, when I set `SelectionMode="Extended"`. This gives me the behavior I am looking for, since I want to deselect all other selected items, when I click on a new item, including one, that was already selected. I don't really understand it, but is there any danger in commenting it in this case? – packoman Feb 19 '16 at 10:00
  • Ok. Sorry, I had missed your comment above: "In case of Double click you dont need it." So thanks again for your help and time. Like I said above, This was great and much appreciated! – packoman Feb 19 '16 at 10:05