0

I am having one problem to bind SelectedItems of ListView control, in order to solve it i have extended ListView control, ListViewExtended to expose SelectedItemsList using the reference from this post but two way binding is not working to get the SelectedItems in the model.

Kindly helps to point out the issue in this demo source.

Custom Control:

public sealed class ListViewExtended : ListView
{
    public ListViewExtended()
    {
        this.SelectionChanged += ListViewExtended_SelectionChanged;
    }

    void ListViewExtended_SelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        this.SelectedItemsList = this.SelectedItems;
    }

    #region SelectedItemsList

    public IEnumerable SelectedItemsList
    {
        get { return (IEnumerable)GetValue(SelectedItemsListProperty); }
        set { SetValue(SelectedItemsListProperty, value); }
    }

    public static readonly DependencyProperty SelectedItemsListProperty =
            DependencyProperty.Register("SelectedItemsList",
            typeof(IEnumerable),
            typeof(ListViewExtended),
            new FrameworkPropertyMetadata(null));

    #endregion
}

Model:

public class Member
{
    public string Name { get; set; }
    public int Age { get; set; }
}

public class Family : BindableBase
{
    private string _name;
    public string Name
    {
        get { return _name; }
        set { _name = value; OnPropertyChanged("Name"); }
    }

    private IList<Member> _selectedMembers;
    public IList<Member> SelectedMembers
    {
        get { return _selectedMembers; }
        set { _selectedMembers = value; OnPropertyChanged("SelectedMembers"); }
    }

    public override string ToString()
    {
        return Name;
    }
}

ViewModel:

public class ListViewSelectedItemsExtendedViewModel : BindableBase
{
    Dictionary<string, IEnumerable> _repository = new Dictionary<string, IEnumerable>();

    public ListViewSelectedItemsExtendedViewModel()
    {
        var families = new List<Family>();
        families.Add(new Family() { Name = "family 1" });
        families.Add(new Family() { Name = "family 2" });
        Families = families;

        var items = new List<Member>();
        items.Add(new Member() { Name = "John", Age = 30 });
        items.Add(new Member() { Name = "Chapel", Age = 50 });
        items.Add(new Member() { Name = "Max", Age = 46 });
        _repository.Add(families[0].Name, items);

        items = new List<Member>();
        items.Add(new Member() { Name = "Warner", Age = 28 });
        items.Add(new Member() { Name = "Peter", Age = 36 });
        items.Add(new Member() { Name = "Tom", Age = 5 });
        _repository.Add(families[1].Name, items);
    }

    private IList<Family> _families;
    public IList<Family> Families
    {
        get { return _families; }
        set { _families = value; OnPropertyChanged("Families"); }
    }

    private Family _selectedFamily;
    public Family SelectedFamily
    {
        get { return _selectedFamily; }
        set { _selectedFamily = value; OnPropertyChanged("SelectedFamily"); }
    }

    private ICommand _familySelectionChangedCommand;
    public ICommand FamilySelectionChangedCommand
    {
        get
        {
            return _familySelectionChangedCommand ?? (_familySelectionChangedCommand = new DelegateCommand(
                () =>
                {
                    Members = (IList<Member>)_repository[SelectedFamily.Name];
                }
            ));
        }
    }
}

XAML:

<Grid>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="1*" />
        <ColumnDefinition Width="3*" />
    </Grid.ColumnDefinitions>
    <ListBox ItemsSource="{Binding Families}" SelectedItem="{Binding SelectedFamily}">
        <i:Interaction.Triggers>
            <i:EventTrigger EventName="SelectionChanged">
                <i:InvokeCommandAction Command="{Binding FamilySelectionChangedCommand}" />
            </i:EventTrigger>
        </i:Interaction.Triggers>
    </ListBox>
    <Grid Grid.Column="1">
        <Grid.RowDefinitions>
            <RowDefinition />
            <RowDefinition />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <fsx:ListViewExtended Margin="10" Grid.Row="0"
                            ItemsSource="{Binding Members}" 
                            SelectedItemsList="{Binding SelectedFamily.SelectedMembers, Mode=TwoWay}">
            <ListView.View>
                <GridView>
                    <GridViewColumn Header="Include">
                        <GridViewColumn.CellTemplate>
                            <DataTemplate>
                                <CheckBox IsChecked="{Binding Path=IsSelected, 
                                RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ListViewItem}}}" />
                            </DataTemplate>
                        </GridViewColumn.CellTemplate>
                    </GridViewColumn>
                    <GridViewColumn Header="Name" Width="120" DisplayMemberBinding="{Binding Name}" />
                    <GridViewColumn Header="Age" Width="50" DisplayMemberBinding="{Binding Age}" />
                </GridView>
            </ListView.View>
            <!--<i:Interaction.Behaviors>
            <fsx:ListViewMultiSelectionBehavior 
                SelectedItems="{Binding SelectedFamily.SelectedMembers}" />
        </i:Interaction.Behaviors>-->
        </fsx:ListViewExtended>
        <ListView Margin="10" Grid.Row="1" 
                ItemsSource="{Binding SelectedFamily.SelectedMembers}">
            <ListView.View>
                <GridView>
                    <GridViewColumn Header="Name" Width="120" DisplayMemberBinding="{Binding Name}" />
                    <GridViewColumn Header="Age" Width="50" DisplayMemberBinding="{Binding Age}" />
                </GridView>
            </ListView.View>
        </ListView>
    </Grid>
</Grid>

Apart from above implementation, i have also tried using the approach given in this post but no luck to get 100% results.

Community
  • 1
  • 1
Furqan Safdar
  • 16,260
  • 13
  • 59
  • 93
  • In my experience IEnumerable is not the suitable for implementing two way databinding. Try using IList or even ObservableCollection. – E-Bat Dec 28 '16 at 14:16
  • I have already tried using IList early on but no luck. – Furqan Safdar Dec 28 '16 at 14:52
  • 1
    @Furqan I ran into the same problem. It's the conversion that causes the issue. Ended up using `IEnumerable` in both the view (Custom Control) and the viewmodel. – Funk Dec 28 '16 at 14:57
  • @Funk, yes it's working but there must be some way to generalize it like other built-in control's DependencyProperty work with collections. – Furqan Safdar Dec 28 '16 at 15:30

1 Answers1

0

The problem here is that you can't set a source property of the generic type IList<Member> to a non-generic IList which is the type of the SelectedItems property of the ListView. If you try this yourself you will notice that it won't even compile:

IList<Member> members = listView1.SelectedItems; //WON'T COMPILE

The generic IList<T> interface does not extend the IList interface so these are two completely different types. It's like trying to set an int property to a string. It just won't work.

If you change the type of the SelectedMembers property of your Family class to the non-generic IList it will work:

public class Family : BindableBase
{
    private string _name;
    public string Name
    {
        get { return _name; }
        set { _name = value; OnPropertyChanged("Name"); }
    }

//change the type here:
    private IList _selectedMembers;
    public IList SelectedMembers
    {
        get { return _selectedMembers; }
        set { _selectedMembers = value; OnPropertyChanged("SelectedMembers"); }
    }

    public override string ToString()
    {
        return Name;
    }
}

there must be some way to generalize it like other built-in control's DependencyProperty work with collections

As mentioned; for you to be able to set the source property to the value of the SelectedItems property of the ListView, the type of the source property must match the type of the SelectedItems property, i.e. the source property should be a non-generic IList.

You may also want to read this: https://blog.magnusmontin.net/2014/01/30/wpf-using-behaviours-to-bind-to-readonly-properties-in-mvvm/. It's about how to bind to read-only properties using behaviours. The type of the properties must still match though no matter what solution you choose to adopt.

mm8
  • 163,881
  • 10
  • 57
  • 88
  • IList extends IEnumerable, `public interface IList : ICollection, IEnumerable, IEnumerable` – Furqan Safdar Dec 28 '16 at 20:20
  • You are right but the issue here is that you are trying to assign an IList property to an IList value and this won't work. Your custom "SelectedItemsList" is indeed an IEnumerable but the *source* property that it binds to is an IList. I have edited my answer to clarify. – mm8 Dec 28 '16 at 20:40
  • Again IList can be assigned to IList. – Furqan Safdar Dec 29 '16 at 05:51
  • My point here is that both ways conversion is possible with explicit cast, so there has to be some way to make it work. – Furqan Safdar Dec 29 '16 at 06:01
  • An IList cannot be assigned to an IList as I explained. And how are you supposed to be able to do an explicit cast if you don't know the type of the source property? If you know that the source property is an IList you could set the SelectedItemsList property to an IList in the SelectionChanged event handler: this.SelectedItemsList = this.SelectedItems.OfType().ToList(); But then the control isn't that "general" after all. – mm8 Dec 29 '16 at 08:23