다음을 통해 공유


WPF: Tips - Bind to Current item of Collection


Introduction

A little known and hence under-used aspect of WPF Binding is the ability to bind to the "current" item in a collection's view.  
Broadly speaking, this substitutes the "/" binding symbol for the dot.  For example between collection and property name.  Read more here.

Explanation

The current item can theoretically be different from whichever the user has selected in a collectionview bound to a Selector but let's concentrate on situations when current means selected item.  The way to make sure the current item and selected item match one another is to use the Selector.IsSynchronizedWithCurrentItem property.
All this is probably clearer if you see code:
In a ViewModel you have a public property:

public ListCollectionView PeopleCollectionView { get; set; }

The source of this will be set to an observable collection, one way or another.  Let's gloss over exactly how for now.
Can't resist?
For an in depth explanation of such matters see WPF: CollectionView Tips.

This collection is bound to the ItemsSource of a Datagrid:

<DataGrid x:Name="dg" AutoGenerateColumns="False"
          ItemsSource="{Binding PeopleCollectionView}"
          IsSynchronizedWithCurrentItem="True"

The thing to particularly notice there is the usage of IsSynchonisedWithCurrentItem.
If the item selected by the user changes the current item will change.  
Similarly, if the current item changes then the selected item in the view will also match that.
As explained in the CollectionView Tips article, this doesn't actually give keyboard focus to the row in the datagrid so there are a couple of attached behaviors which need to be applied in order to make that aspect "work" completely.
You can then use the various MoveCurrentTo.... methods on that ListCollectionView from the viewmodel and the view will respond.
That makes it pretty easy to write collection navigation commands in a ViewModel.
For example:

    NextRecord = new RelayCommand(() => NextRecordExecute());
    PreviousRecord = new RelayCommand(() => PreviousRecordExecute());
}
public RelayCommand NextRecord { get; set; }
public RelayCommand PreviousRecord { get; set; }
public void PreviousRecordExecute()
{
    PeopleCollectionView.MoveCurrentToPrevious();
}
public void NextRecordExecute()
{
    PeopleCollectionView.MoveCurrentToNext();
}

Note that this code doesn't depend on knowing which specific record is current at the moment, making it nicely decoupled.

What About That Slash?

You can Bind to the current item in a collection.

<TextBox Text="{Binding PeopleCollectionView/FirstName, UpdateSourceTrigger=PropertyChanged}"/>

Even bold and with the font increased, it's pretty easy to miss.  Notice the / in there after the PeopleCollectionView and before FirstName.

Why Use This?

You could of course usually just bind to the SelectedItem of another control directly.  
You could bind selecteditem of the datagrid below to a property of a viewmodel and then bind that textbox to SelectedPerson.FirstName instead.
The downside being you need to have a reference to that particular control which in turn creates a dependency.  No big deal if they're in the same chunk of mark up.
Rather a complication if they're in two different usercontrols.

The reason to use this is if you want both controls to share that selected item without any dependency other than the collection.
If that sounds a bit crazy, you sometimes want this sort of independence for a composable UI.
In that scenario you can build your collections completely discrete from Views, ViewModels etc.
The collections can be stashed away in Application.Current.Resources.
They can potentially be filled by a background process which has no reference to any view or viewmodel and runs independently.
Any ViewModel which needs to use such a collection can then go get a reference to one of them easily since any bit of code in a WPF application can dip into Application.Current.Resources.
They or their viewmodel can then change the current item and it will be reflected in any other viewmodel using that collection.
You can see a simplified example of this working in the samples:
Dynamic XAML: Composed View whose xaml is generated dynamically by Dynamic XAML: View Composer.

In this View:

There are 4 usercontrols presented in MainWindow there.
The top two are PeopleView and PersonView.
That PeopleCollectionView is shared by both PeopleViewModel and PersonViewModel.
PeopleViewModel:

public class PeopleViewModel : NotifyUIBase
{
    public ListCollectionView PeopleCollectionView {get; set;}
    private Person CurrentPerson
    {
        get { return PeopleCollectionView.CurrentItem as Person; }
        set
        {
            PeopleCollectionView.MoveCurrentTo(value);
            RaisePropertyChanged();
 
        }
    }
    public PeopleViewModel()
    {
        PeopleCollectionView = Application.Current.Resources["PeopleCollectionView"] as ListCollectionView;
        PeopleCollectionView.MoveCurrentToPosition(1);
    }
}

As you can see, that collectionview is retrieved from Application.Current.Resources and the Datagrid ItemsSource will be bound to that.
PersonViewModel:

public class PersonViewModel : NotifyUIBase
{
    public ListCollectionView PeopleCollectionView { get; set; }
 
    private Person CurrentPerson
    {
        get { return PeopleCollectionView.CurrentItem as Person; }
        set
        {
            PeopleCollectionView.MoveCurrentTo(value);
            RaisePropertyChanged();
 
        }
    }
    public PersonViewModel()
    {
        PeopleCollectionView = Application.Current.Resources["PeopleCollectionView"] as ListCollectionView;
        NextRecord = new RelayCommand(() => NextRecordExecute());
        PreviousRecord = new RelayCommand(() => PreviousRecordExecute());
    }
    public RelayCommand NextRecord { get; set; }
    public RelayCommand PreviousRecord { get; set; }
    public void PreviousRecordExecute()
    {
        PeopleCollectionView.MoveCurrentToPrevious();
    }
    public void NextRecordExecute()
    {
        PeopleCollectionView.MoveCurrentToNext();
    }
}

 Again, this gets the collectionview holding people and presents it so PeopleView can bind it.
The View binds to the current person:

<UserControl x:Class="Wpf_Dynamic_XAML_Composed_View.PersonView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:local="clr-namespace:Wpf_Dynamic_XAML_Composed_View"
             mc:Ignorable="d"
             d:DesignHeight="300" d:DesignWidth="300">
 
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*"/>
            <RowDefinition Height="30"/>
        </Grid.RowDefinitions>
        <Grid.Resources>
            <Style TargetType="{x:Type ListBoxItem}">
                <Setter Property="Template">
                    <Setter.Value>
                        <ControlTemplate TargetType="{x:Type ListBoxItem}">
                            <Grid Background="{TemplateBinding Background}">
                                <ContentPresenter 
                                        ContentTemplate="{TemplateBinding ContentTemplate}"
                                        Content="{TemplateBinding Content}"
                                        HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
                                        Margin="{TemplateBinding Padding}">
                                </ContentPresenter>
                            </Grid>
                        </ControlTemplate>
                    </Setter.Value>
                </Setter>
            </Style>
        </Grid.Resources>
 
        <ListBox HorizontalContentAlignment="Stretch" Background="AliceBlue">
            <ListBox.Resources>
                <Style TargetType="{x:Type TextBox}">
                    <Setter Property="Margin" Value="8,0,8,4" />
                    <Setter Property="HorizontalAlignment" Value="Stretch"/>
                </Style>
            </ListBox.Resources>
 
            <local:EditRow LabelFor="First Name:" >
                <TextBox Text="{Binding PeopleCollectionView/FirstName, UpdateSourceTrigger=PropertyChanged}"/>
            </local:EditRow>
            <local:EditRow  LabelFor="Middle Name:">
                <TextBox Text="{Binding PeopleCollectionView/MiddleName, UpdateSourceTrigger=PropertyChanged}"/>
            </local:EditRow>
 
            <local:EditRow  LabelFor="SurName:">
                <TextBox Text="{Binding PeopleCollectionView/LastName, UpdateSourceTrigger=PropertyChanged}"/>
            </local:EditRow>
            <local:EditRow  LabelFor="Login Id:">
                <TextBox Text="{Binding PeopleCollectionView/LoginId, UpdateSourceTrigger=PropertyChanged}"/>
            </local:EditRow>
            <local:EditRow  LabelFor="Job Title:">
                <TextBox Text="{Binding PeopleCollectionView/JobTitle, UpdateSourceTrigger=PropertyChanged}"/>
            </local:EditRow>
            <local:EditRow  LabelFor="Gender:">
                <TextBox Text="{Binding PeopleCollectionView/Gender, UpdateSourceTrigger=PropertyChanged}"/>
            </local:EditRow>
            <local:EditRow  LabelFor="Hire Date:">
                <TextBox Text="{Binding PeopleCollectionView/HireDate, UpdateSourceTrigger=PropertyChanged}"/>
            </local:EditRow>
            <local:EditRow  LabelFor="Birthday:">
                <TextBox Text="{Binding PeopleCollectionView/BirthDate, UpdateSourceTrigger=PropertyChanged}"/>
            </local:EditRow>
        </ListBox>
 
        <Button  Grid.Row="1" Width="24" Height="24"
                              Command="{Binding PreviousRecord}"
                              BorderThickness="0"
                              HorizontalAlignment="Left"
                                        >
            <Path Data="{StaticResource PreviousIcon}" Stretch="Uniform"
                                        Fill="Gray" />
            <Button.ToolTip>
                <TextBlock Text="Previous Record"/>
            </Button.ToolTip>
        </Button>
        <Button  Grid.Row="1"  Width="24" Height="24"
                               Command="{Binding NextRecord}"
                               BorderThickness="0"
                               HorizontalAlignment="Right"
                                        >
            <Path Data="{StaticResource NextIcon}" Stretch="Uniform"
                                        Fill="Gray" />
            <Button.ToolTip>
                <TextBlock Text="Next Record"/>
            </Button.ToolTip>
        </Button>
    </Grid>
</UserControl>

Notice that all those fields use the slash notation.
If you download and run the sample you will find as you click the next and previous buttons not only does the record in PersonView change but the selected item in the datagrid also changes.
As you overtype a field like name, the change appears in the datagrid.

These two usercontrols and viewmodels are totally decoupled but sharing the same objects.

Property List Editing

You probably noticed that UI is slightly unusual.  There's a list of EditRow usercontrols in a ListBox.
This technique is explained in detail here.

See Also

This article is part of the WPF Tips Series, if WPF is your area of interest then you will probably find other useful articles there.