Как запретить WPF DataGrid отменить выбор SelectedItem при обновлении элементов? - PullRequest
8 голосов
/ 02 сентября 2010

Мой сценарий: у меня есть фоновый поток, который запрашивает изменения и периодически обновляет коллекцию WPF DataGrid ObservableCollection (в стиле MVVM). Пользователь может щелкнуть строку в DataGrid и вызвать «детали» этой строки в соседнем элементе управления UserControl в том же главном окне.

Когда фоновый поток имеет обновления, он циклически перебирает объекты в ObservableCollection и заменяет отдельные объекты, если они изменились (другими словами, я не привязываю новую целую ObservableCollection к DataGrid, а заменяю отдельные элементы в коллекции (это позволяет DataGrid поддерживать порядок сортировки во время обновлений).

Проблема заключается в том, что после того, как пользователь выбрал определенную строку и детали отображаются в смежном элементе управления пользователя, когда фоновый поток обновляет DataGrid, DataGrid теряет SelectedItem (он возвращается к индексу -1).

Как сохранить SelectedItem между обновлениями ObservableCollection?

Ответы [ 3 ]

6 голосов
/ 02 сентября 2010

Если ваша сетка является одиночным выбором, я предлагаю вам использовать CollectionView в качестве ItemsSource вместо фактического ObservableCollection. Затем убедитесь, что для Datagrid.IsSynchronizedWithCurrentItem установлено значение true. Наконец, в конце вашей «логики замены элемента» просто переместите CurrentItem объекта CollectionView в соответствующий новый элемент.

Ниже приведен пример, демонстрирующий это. (Я использую здесь ListBox. Надеюсь, он отлично работает с вашим Datagrid).

РЕДАКТИРОВАТЬ - НОВЫЙ ОБРАЗЕЦ С ИСПОЛЬЗОВАНИЕМ MVVM:

1009 * XAML *

<Window x:Class="ContextTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        x:Name="window"
        Title="MainWindow" Height="350" Width="525">
    <DockPanel>
        <ListBox x:Name="lb" DockPanel.Dock="Left" Width="200" 
                 ItemsSource="{Binding ModelCollectionView}"
                 SelectionMode="Single" IsSynchronizedWithCurrentItem="True">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <TextBlock Text="{Binding Path=Name}"/>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>

        <TextBlock Text="{Binding ElementName=lb, Path=SelectedItem.Description}"/>

    </DockPanel>
</Window>

Code-Behind:

using System;
using System.Windows;
using System.Windows.Data;
using System.Collections.ObjectModel;
using System.Windows.Threading;

namespace ContextTest
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            this.DataContext = new ViewModel();
        }
    }

    public class ViewModel
    {
        private DataGenerator dataGenerator;
        private ObservableCollection<Model> modelCollection;
        public ListCollectionView ModelCollectionView { get; private set; }

        public ViewModel()
        {
            modelCollection = new ObservableCollection<Model>();
            ModelCollectionView = new ListCollectionView(modelCollection);

            //Create models
            for (int i = 0; i < 20; i++)
                modelCollection.Add(new Model() { Name = "Model" + i.ToString(), 
                    Description = "Description for Model" + i.ToString() });

            this.dataGenerator = new DataGenerator(this);
        }

        public void Replace(Model oldModel, Model newModel)
        {
            int curIndex = ModelCollectionView.CurrentPosition;
            int n = modelCollection.IndexOf(oldModel);
            this.modelCollection[n] = newModel;
            ModelCollectionView.MoveCurrentToPosition(curIndex);
        }
    }

    public class Model
    {
        public string Name { get; set; }
        public string Description { get; set; }
    }

    public class DataGenerator
    {
        private ViewModel vm;
        private DispatcherTimer timer;
        int ctr = 0;

        public DataGenerator(ViewModel vm)
        {
            this.vm = vm;
            timer = new DispatcherTimer(TimeSpan.FromSeconds(5), 
                DispatcherPriority.Normal, OnTimerTick, Dispatcher.CurrentDispatcher);
        }

        public void OnTimerTick(object sender, EventArgs e)
        {
            Random r = new Random();

            //Update several Model items in the ViewModel
            int times = r.Next(vm.ModelCollectionView.Count - 1);
            for (int i = 0; i < times; i++)
            {   
                Model newModel = new Model() 
                    { 
                        Name = "NewModel" + ctr.ToString(),
                        Description = "Description for NewModel" + ctr.ToString()
                    };
                ctr++;

                //Replace a random item in VM with a new one.
                int n = r.Next(times);
                vm.Replace(vm.ModelCollectionView.GetItemAt(n) as Model, newModel);
            }
        }
    }
}

СТАРЫЙ ОБРАЗЕЦ:

XAML:

<Window x:Class="ContextTest.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">
    <StackPanel>
        <ListBox x:Name="lb" SelectionMode="Single" IsSynchronizedWithCurrentItem="True" SelectionMode="Multiple">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <TextBlock Text="{Binding Path=Name}"/>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>

        <TextBlock Text="{Binding ElementName=lb, Path=SelectedItem.Name}"/>
        <Button Click="Button_Click">Replace</Button>


    </StackPanel>
</Window>

Код-за:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Collections.ObjectModel;
using System.ComponentModel;

namespace ContextTest
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        ObservableCollection<MyClass> items;
        ListCollectionView lcv;

        public MainWindow()
        {
            InitializeComponent();

            items = new ObservableCollection<MyClass>();
            lcv = (ListCollectionView)CollectionViewSource.GetDefaultView(items);
            this.lb.ItemsSource = lcv;
            items.Add(new MyClass() { Name = "A" });
            items.Add(new MyClass() { Name = "B" });
            items.Add(new MyClass() { Name = "C" });
            items.Add(new MyClass() { Name = "D" });
            items.Add(new MyClass() { Name = "E" });

        }

        public class MyClass
        {
            public string Name { get; set; }
        }

        int ctr = 0;
        private void Button_Click(object sender, RoutedEventArgs e)
        {
            MyClass selectedItem = this.lb.SelectedItem as MyClass;
            int index = this.items.IndexOf(selectedItem);
            this.items[index] = new MyClass() { Name = "NewItem" + ctr++.ToString() };
            lcv.MoveCurrentToPosition(index);
        }

    }
}
3 голосов
/ 02 сентября 2010

Я не работал с WPF DataGrid, но я бы попробовал такой подход:

Добавить свойство в модель представления, которое будет содержать значение выбранного в данный момент элемента.

Привязать SelectedItem к этому новому свойству, используя TwoWay.

Таким образом, когда пользователь выбирает строку, он обновляет модель представления, а когда обновляется ObservableCollection, он выигрываетt влияет на свойство, с которым связан SelectedItem.Будучи связанным, я не ожидал бы, что он может сброситься так, как вы видите.

1 голос
/ 03 сентября 2010

В логике, которая обновляет коллекцию, можно сохранить ссылку элемента CollectionView.Current на другую переменную.Затем, после завершения обновления, вызовите CollectionView.MoveCurrentTo (переменная) для сброса выбранного элемента.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...