【问题标题】:WPF DataGrid bind to ItemsSource items propertyWPF DataGrid 绑定到 ItemsSource 项目属性
【发布时间】:2017-09-18 13:06:26
【问题描述】:

我有实现IList 的集合,它从服务器异步加载数据。当通过索引访问集合时,它会返回一个存根对象并在后台启动数据请求。请求完成后收集刷新内部状态并调用PropertyChanged 事件。目前,集合返回的项目如下所示:

public class VirtualCollectionItem 
{
    // actual entity
    public object Item { get; } 

    // some metadata properties
    public bool IsLoading { get; } 
}

问题是我不知道如何将这样的集合绑定到DataGrid。 我想以某种方式将DataGrid.ItemsSource 设置为VirtualCollectionItem 的集合,使DataGrid 显示实际项目(也在SelectedItem 中)并保留使用元数据的可能性(即使用IsLoading 来可视化数据加载)。 我尝试在DataGrid.RowStyle 中设置DataGridRow.Item 绑定,但没有成功。

<DataGrid.RowStyle>
  <Style TargetType="{x:Type DataGridRow}">
    <Setter Property="Item" Value="{Binding Item}" />
    <Style.Triggers>
      <DataTrigger Binding="{Binding IsLoading}" Value="True">
        <DataTrigger.Setters>
          <Setter Property="Background" Value="Gray" />
        </DataTrigger.Setters>
      </DataTrigger>
    </Style.Triggers>
  </Style>
</DataGrid.RowStyle>

另一种选择是将VirtualCollectionItem 属性扁平化为VirtualCollection 本身:

class VirtualCollection
{
    // ...

    // wrapper around VirtualCollectionItem
    public IList<object> Items { get; }

    public IList<bool> IsLoadingItems { get; } 

    // ...
}

并在DataGrid 中使用这些属性,但我还没有意识到如何使其工作。

【问题讨论】:

    标签: c# wpf xaml datagrid


    【解决方案1】:

    好的,所以你从服务器加载实体,但你仍然需要从ViewModel 访问集合。让我们将此功能移动到服务中。该服务允许您异步加载实体 ID 列表,或加载特定实体详细信息:

    using AsyncLoadingCollection.DTO;
    using System.Collections.Generic;
    using System.Linq;
    using System.Threading.Tasks;
    
    public class PeopleService
    {
        private List<PersonDTO> _people;
    
        public PeopleService()
        {
            InitializePeople();
        }
    
        public async Task<IList<int>> GetIds()
        {
            // simulate async loading delay
            await Task.Delay(1000);
            var result = _people.Select(p => p.Id).ToList();
            return result;
        }
        public async Task<PersonDTO> GetPersonDetail(int id)
        {
            // simulate async loading delay
            await Task.Delay(3000);
            var person = _people.Where(p => p.Id == id).First();
            return person;
        }
        private void InitializePeople()
        {
            // poor person's database
            _people = new List<PersonDTO>();
            _people.Add(new PersonDTO { Name = "Homer", Age = 39, Id = 1 });
            _people.Add(new PersonDTO { Name = "Marge", Age = 37, Id = 2 });
            _people.Add(new PersonDTO { Name = "Bart", Age = 12, Id = 3 });
            _people.Add(new PersonDTO { Name = "Lisa", Age = 10, Id = 4 });
        }
    }
    

    GetPersonDetail 方法返回一个DTO

    public class PersonDTO
    {
        public string Name { get; set; }
        public int Age { get; set; }
    
        public int Id { get; set; }
    }
    

    DTO 可以转换为您的ViewModel 所需的对象(我使用 Prism 作为 MVVM 框架):

    using Prism.Mvvm;
    
    public class Person : BindableBase
    {
        private string _name;
        public string Name
        {
            get { return _name; }
            set { SetProperty(ref _name, value); }
        }
    
        private int _age;
        public int Age
        {
            get { return _age; }
            set { SetProperty(ref _age, value); }
        }
    
        private int _id;
        public int Id
        {
            get { return _id; }
            set { SetProperty(ref _id, value); }
        }
    
        private bool _isLoaded;
        public bool IsLoaded
        {
            get { return _isLoaded; }
            set { SetProperty(ref _isLoaded, value); }
        }
    }
    

    您可以像这样将 DTO 对象转换为模型:

    using DTO;
    using Model;
    
    // we might use AutoMapper instead
    public static class PersonConverter
    {
        public static Person ToModel(this PersonDTO dto)
        {
            Person result = new Person
            {
                Id = dto.Id,
                Name = dto.Name,
                Age = dto.Age
            };
            return result;
        }
    }
    

    这就是我们在ViewModel 中定义命令(使用物品检索服务)的方式:

    using Helpers;
    using Model;
    using Prism.Commands;
    using Prism.Mvvm;
    using Services;
    using System.Collections.ObjectModel;
    using System.Linq;
    
    public class MainWindowViewModel : BindableBase
    {
        #region Fields
        private  PeopleService _peopleService;
        #endregion // Fields
    
        #region Constructors
        public MainWindowViewModel()
        {
            // we might use dependency injection instead
            _peopleService = new PeopleService();
    
            People = new ObservableCollection<Person>();
            LoadListCommand = new DelegateCommand(LoadList);
            LoadPersonDetailsCommand = new DelegateCommand(LoadPersonDetails, CanLoadPersonDetails)
                .ObservesProperty(() => CurrentPerson)
                .ObservesProperty(() => IsBusy);
        }
        #endregion // Constructors
    
        #region Properties
    
        private string _title = "Prism Unity Application";
        public string Title
        {
            get { return _title; }
            set { SetProperty(ref _title, value); }
        }
    
        private Person _currentPerson;
        public Person CurrentPerson
        {
            get { return _currentPerson; }
            set { SetProperty(ref _currentPerson, value); }
        }
    
        private bool _isBusy;
        public bool IsBusy
        {
            get { return _isBusy; }
            set { SetProperty(ref _isBusy, value); }
        }
        public ObservableCollection<Person> People { get; private set; }
    
        #endregion // Properties
    
        #region Commands
    
        public DelegateCommand LoadListCommand { get; private set; }
        private async void LoadList()
        {
            // reset the collection
            People.Clear();
    
            var ids = await _peopleService.GetIds();
            var peopleListStub = ids.Select(i => new Person { Id = i, IsLoaded = false, Name = "No details" });
    
            People.AddRange(peopleListStub);
        }
    
        public DelegateCommand LoadPersonDetailsCommand { get; private set; }
        private bool CanLoadPersonDetails()
        {
            return ((CurrentPerson != null) && !IsBusy);
        }
        private async void LoadPersonDetails()
        {
            IsBusy = true;
    
            var personDTO = await _peopleService.GetPersonDetail(CurrentPerson.Id);
            var updatedPerson = personDTO.ToModel();
            updatedPerson.IsLoaded = true;
    
            var oldPersonIndex = People.IndexOf(CurrentPerson);
            People.RemoveAt(oldPersonIndex);
            People.Insert(oldPersonIndex, updatedPerson);
            CurrentPerson = updatedPerson;
    
            IsBusy = false;
        }
    
        #endregion // Commands
    }
    

    最后,View 可能就这么简单:

    <Window x:Class="AsyncLoadingCollection.Views.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:prism="http://prismlibrary.com/"
        Title="{Binding Title}"
        Width="525"
        Height="350"
        prism:ViewModelLocator.AutoWireViewModel="True">
    <StackPanel>
        <!--<ContentControl prism:RegionManager.RegionName="ContentRegion" />-->
        <StackPanel HorizontalAlignment="Center" Orientation="Horizontal">
            <Button Width="100"
                    Margin="10"
                    Command="{Binding LoadListCommand}"
                    Content="Load List" />
            <Button Width="100"
                    Margin="10"
                    Command="{Binding LoadPersonDetailsCommand}"
                    Content="Load Details" />
        </StackPanel>
        <TextBlock Text="{Binding CurrentPerson.Name}" />
        <DataGrid CanUserAddRows="False"
                  CanUserDeleteRows="False"
                  ItemsSource="{Binding People}"
                  SelectedItem="{Binding CurrentPerson,
                                         Mode=TwoWay}">
            <DataGrid.RowStyle>
                <Style TargetType="{x:Type DataGridRow}">
                    <!--<Setter Property="Item" Value="{Binding Item}" />-->
                    <Style.Triggers>
                        <DataTrigger Binding="{Binding IsLoaded}" Value="False">
                            <DataTrigger.Setters>
                                <Setter Property="Background" Value="DarkGray" />
                            </DataTrigger.Setters>
                        </DataTrigger>
                    </Style.Triggers>
                </Style>
            </DataGrid.RowStyle>
        </DataGrid>
    </StackPanel>
    </Window>
    

    【讨论】:

      【解决方案2】:

      您可以将 VirtualCollection 直接绑定到 DataGrid.ItemsSource 属性。然后也绑定 SelectedItem 属性:

      <DataGrid ItemsSource="{Binding MyVirtualCollectionList}" SelectedItem={Binding SelectedItem, Mode=TwoWay} />
      

      然后是 ViewModel:

      public class MyViewModel : INotifyPropertyChanged
      {
          public event PropertyChangedEventHandler PropertyChanged;
      
          protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
          {
              PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
          }
      
          private VirtualCollection _myVirtualCollectionList;
      
          public VirtualCollection MyVirtualCollectionList
          {
              get { return _myVirtualCollectionList; }
              set
              {
                  _myVirtualCollectionList = value;
                  OnPropertyChanged();
              }
          }
      
          private VirtualCollectionItem _selectedItem;
      
          public VirtualCollectionItem SelectedItem
          {
              get { return _selectedItem; }
              set
              {
                  _selectedItem = value;
                  OnPropertyChanged();
              }
          }
      }
      

      您必须在加载列表后触发 OnPropertyChanged 事件,并且必须从 MyViewModel 对象执行此操作(我认为您不会这样做)!你也可以使用 ObservableCollection(你可以扩展它)。然后您不需要 OnPropertyChange 事件。从集合中添加/删除项目会自动通知 UI。

      SelectedItem 独立于列表工作。在一行的 DataTemplate 中,您将使用 {Binding IsLoading} 和 {Binding Item.SomeProperty}。

      【讨论】:

      • 在我的情况下,DataGrid 的 Items/SelectedItem(s) 必须是从服务器加载的实际实体,因此我可以使用它而无需重写大量现有代码库。我还在需要的地方调用 PropertyChanged 和 CollectionChanged 事件。
      【解决方案3】:

      我决定用 ViewModel 包装 DataGrid:

      public class DataGridAsyncViewModel : Notifier
      {
        public VirtualCollection ItemsProvider { get; }
      
        private VirtualCollectionItem _selectedGridItem;
      
        public VirtualCollectionItem SelectedGridItem
        {
          get { return _selectedGridItem; }
          set { Set(ref _selectedGridItem, value); }
        }
      
        public object SelectedItem => SelectedGridItem?.IsLoading == false ? SelectedGridItem?.Item : null;
      
        public DataGridAsyncViewModel([NotNull] VirtualCollection itemsProvider)
        {
          if (itemsProvider == null) throw new ArgumentNullException(nameof(itemsProvider));
          ItemsProvider = itemsProvider;
        }
      }
      

      并将其绑定到 DataGrid:

      <DataGrid DataContext="{Binding DataGridViewModel}" 
                SelectedItem="{Binding SelectedGridItem}" 
                ItemsSource="{Binding ItemsProvider}" >
        <DataGrid.RowStyle>
          <Style TargetType="{x:Type DataGridRow}">
            <Style.Triggers>
              <DataTrigger Binding="{Binding IsLoading}" Value="True">
                <Setter Property="Background" Value="LightGray" />
              </DataTrigger>
            </Style.Triggers>
          </Style>
        </DataGrid.RowStyle>
        <DataGrid.Columns>
          <DataGridTextColumn Header="..." Binding="{Binding Item.SomeValue}" />
        </DataGrid.Columns>
      </DataGrid>
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2018-03-10
        • 2011-12-17
        • 1970-01-01
        • 2011-07-10
        • 1970-01-01
        • 2015-11-13
        • 1970-01-01
        • 2019-03-23
        相关资源
        最近更新 更多