0

I'm don't know how to get focused item automatically change within a ListView.

I would like the focused item in the view to automatically change when I change the "IsSelected" property to an other element in the databinded list:

ListView with selected item

When an item is modified by PC/SC card reader (see this as input), the next element should be focused like this:

enter image description here

I would like to stay in MVVM and therefor not having View referenced in the ViewModel. Below is my current code.

Model : The main purpose is to extend a DTO with an IsSelected property and implementing INotifyPropertyChanged

public class SmartDeviceModel : INotifyPropertyChanged
{
    public bool IsSelected;
    private DtoReader _dtoReader;

    public SmartDeviceModel(DtoReader _reader)
    {
        _dtoReader = _reader;
    }

    public string DisplayName => _dtoReader.DisplayName;

    public string Uid
    {
        get
        {
            return _dtoReader.Uid;
        }
        set
        {
            _dtoReader.Uid = value;
            OnPropertyChanged("Uid");
        }
    }

    public long RadioId
    {
        get
        {
            return _dtoReader.RadioId : _dtoMarker.RadioId;
        }
        set
        {
            _dtoReader.RadioId = value;
            OnPropertyChanged("RadioId");
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;

    protected void OnPropertyChanged(string name)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
    }
}

ViewModel received events of a PC/SC card reader to pair data from RFID chip with current selected item. When RFID chip is removed from PC/SC Reader, the next element is well selected but got not focused.

public class ScanDeviceViewModel : BaseViewModel
{
    public BindingList<SmartDeviceModel> ReaderList { get; }
    public int SelectedReaderIndex;

    private ITagReaderInput _rfidReader;

    public ScanDeviceViewModel()
    {
        //Get Data listener for RFID Tag
        _rfidReader = new IdentivTagReader.IdentivTagReader();
        // Data Source of DTO
        SiteInteractor siteInterractor = new SiteInteractor();

        // List used for DataBinding
        ReaderList = new BindingList<SmartDeviceModel>();

        foreach (DtoReader m in SiteInteractor.GetReaders().OrderBy(x => x.DisplayName))
        {
            ReaderList.Add(new SmartDeviceModel(m));
        }

        if (ReaderList.Count() > 0)
        {
            for (var i = 0; i < ReaderList.Count(); i++)
            {
                if (String.IsNullOrEmpty(ReaderList[i].Uid))
                {
                    SelectedReaderIndex = i;
                    ReaderList[i].IsSelected = true;
                    break;
                }
            }
        }
        _rfidReader.LabelDetected += RfidTagDetected;
        _rfidReader.LabelRemoved += RfidRemoved;
    }

    private void RfidTagDetected(ITagLabel tag)
    {
        if (ReaderList[SelectedReaderIndex] != null && string.IsNullOrEmpty(ReaderList[SelectedReaderIndex].Uid))
        {
            ReaderList[SelectedReaderIndex].IsSelected = true;
            ReaderList[SelectedReaderIndex].Uid = tag.Uid;
            ReaderList[SelectedReaderIndex].RadioId = tag.RadioId;
        }

    }

    private void RfidRemoved(ITagLabel tag)
    {
       if (ReaderList[SelectedReaderIndex].Uid == tag.Uid)
        {
            ReaderList[SelectedReaderIndex].IsSelected = false;
            while (ReaderList.Count >= SelectedReaderIndex + 1)
            {
                SelectedReaderIndex++;
                if (String.IsNullOrEmpty(ReaderList[SelectedReaderIndex].Uid)){
                    ReaderList[SelectedReaderIndex].IsSelected = true;
                    break;
                }
            }
        }
    }
}

View I'm using a "Setter" using databinding to my model property "IsSelected" as suggested here but I most missed something else I don't understand yet.

<ListView ItemsSource="{Binding ReaderList}"  
 Margin="5" x:Name="listViewReader" SelectionMode="Single" 
      <ListView.ItemContainerStyle>
        <Style TargetType="{x:Type ListViewItem}">
           <Setter Property="BorderBrush" Value="LightGray" />
           <Setter Property="BorderThickness" Value="0,0,0,1" />
           <Setter Property="IsSelected" Value="{Binding IsSelected}" />
        </Style>
      </ListView.ItemContainerStyle>
      <ListView.ItemTemplate>
        <DataTemplate>
          <Viewbox Grid.Row ="0" Stretch="Uniform" HorizontalAlignment="Left" VerticalAlignment="Bottom" MaxHeight="90">
              <Grid>
                 <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="*"/>
                    <ColumnDefinition Width="*"/>
                 </Grid.ColumnDefinitions>
                 <Grid.RowDefinitions>
                     <RowDefinition Height="2*" />
                     <RowDefinition Height="*"/>
                 </Grid.RowDefinitions>
                 <Label Content="{Binding DisplayName}" />
                 <DockPanel  Grid.Row="1">
                   <Label Content="UID"/>
                   <Label Content="{Binding Uid}"/>
                 </DockPanel>
                 <DockPanel Grid.Row="1" Grid.Column="1">
                   <Label Content="RadioID" />
                   <Label Content="{Binding RadioId}"/>
                 </DockPanel>
              </Grid>
            </Viewbox>
          </DataTemplate>
    </ListView.ItemTemplate>
</ListView>

I tried several approach like this answer, although item is well selected, it is not focused.

Guillaume Raymond
  • 1,726
  • 1
  • 20
  • 33
  • 1
    Bind to a listcollectionview based on the default view of your collection. Set is synchronised with current item on the listview so the current item will be the selected one and vice versa. Add a behaviour that focusses the selected item. Google, you'll find it easy. This does a datagrid but both datagrid and listview are itemscontrols. https://social.technet.microsoft.com/wiki/contents/articles/26673.wpf-collectionview-tips.aspx#Programmatic_Selection_of_Record – Andy Apr 09 '18 at 12:28
  • https://stackoverflow.com/questions/10444518/how-do-you-programmatically-set-focus-to-the-selecteditem-in-a-wpf-listbox-that – Andy Apr 09 '18 at 12:28
  • @Andy : ListCollectionView seems promising I'll give it a try right away. Thanks for pointing me in the good direction – Guillaume Raymond Apr 09 '18 at 12:34

2 Answers2

0

I finally figure it out. Below is my current working code.

In the Model I have just changed the flag IsSelected to IsCurrent to avoid confusion with ListViewItem built-in property but it might just be an implementation detail.

public class SmartDeviceModel : INotifyPropertyChanged
{
    public bool IsCurrent;
    [...]
}

The BindingList in ViewModel is mostly the same as in OP:

public class ScanDeviceViewModel : INotifyPropertyChanged
{
   public BindingList<SmartDeviceModel> ReaderList { get; internal set; }
   [...]
}

NB : BindingList seems to reduce OnNotifyPropertyChange need but other Type of List should work with a tiny bit of extra code. I also noticed BindingList might not be suited for huge list scenario.

The View is then using the above ViewModel as DataContext and therefore Binding ItemSource to the BindingList. The ListViewItem Style Setter is then using the IsCurrent Property from the Model.

 <ListView ItemsSource="{Binding ReaderList}"  
    SelectionMode="Single"
    SelectionChanged="OnListViewSelectionChanged">
       <ListView.ItemContainerStyle>
              <Style TargetType="{x:Type ListViewItem}">
                   <Setter Property="IsSelected" Value="{Binding IsCurrent}" />
               </Style>
        </ListView.ItemContainerStyle>
 [...]

And finally this piece of View Code behind below is mainly to simulate the focus as per user input, otherwise the elemant get selected but not focused and might be outside the visible item scope :

private void OnListViewSelectionChanged(object sender, SelectionChangedEventArgs e)
{
    ListView listView = e.Source as ListView;
    if (listView.ItemContainerGenerator.ContainerFromItem(listView.SelectedItem) is FrameworkElement container)
    {
        container.Focus();
    }
}
Guillaume Raymond
  • 1,726
  • 1
  • 20
  • 33
0

According to MVVM you can implement custom Interaction Behavior:

  1. Import to XAML: xmlns:b="http://schemas.microsoft.com/xaml/behaviors" (if you are using .NET Core 3.1 - 5)

  2. Add to content-body:

    <ListView ...>
      <b:Interaction.Behaviors>
        <local:AutoScrollToLastItemBehavior />
      </b:Interaction.Behaviors>
    </ListView>
    
  3. Finally add the next class:

    public sealed class AutoScrollToLastItemBehavior : Microsoft.Xaml.Behaviors.Behavior<ListView>
    {
    // Need to track whether we've attached to the collection changed event
    bool _collectionChangedSubscribed = false;
    
    protected override void OnAttached()
    {
        base.OnAttached();
        AssociatedObject.SelectionChanged += SelectionChanged;
    
        // The ItemSource of the listView will not be set yet, 
        // so get a method that we can hook up to later
        AssociatedObject.DataContextChanged += DataContextChanged;
    }
    
    private void SelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        ScrollIntoView();
    }
    
    private void CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        ScrollIntoView();
    }
    
    private void DataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
    {
        // The ObservableCollection implements the INotifyCollectionChanged interface
        // However, if this is bound to something that doesn't then just don't hook the event
        var collection = AssociatedObject.ItemsSource as INotifyCollectionChanged;
        if (collection != null && !_collectionChangedSubscribed)
        {
            // The data context has been changed, so now hook 
            // into the collection changed event
            collection.CollectionChanged += CollectionChanged;
            _collectionChangedSubscribed = true;
        }
    
    }
    
    private void ScrollIntoView()
    {
        int count = AssociatedObject.Items.Count;
        if (count > 0)
        {
            var last = AssociatedObject.Items[count - 1];
            AssociatedObject.ScrollIntoView(last);
        }
    }
    
    protected override void OnDetaching()
    {
        base.OnDetaching();
        AssociatedObject.SelectionChanged -= SelectionChanged;
        AssociatedObject.DataContextChanged -= DataContextChanged;
    
        // Detach from the collection changed event
        var collection = AssociatedObject.ItemsSource as INotifyCollectionChanged;
        if (collection != null && _collectionChangedSubscribed)
        {
            collection.CollectionChanged -= CollectionChanged;
            _collectionChangedSubscribed = false;
    
        }
    }
    }