我有实现IList
的集合,它从服务器异步加载数据。当通过索引访问集合时,它返回一个存根对象并在后台启动数据请求。请求完成后,将刷新内部状态并调用PropertyChanged
事件。现在,集合返回的项目如下所示:
public class VirtualCollectionItem
{
// actual entity
public object Item { get; }
// some metadata properties
public bool IsLoading { get; }
}
问题在于我无法实现如何将此类集合绑定到DataGrid
。
我想以某种方式将DataGrid.ItemsSource
设置为VirtualCollectionItem
的集合,使DataGrid
显示实际的项目(也在SelectedItem
中)并留下使用元数据的可能性(即使用{ {1}}可视化数据加载)。
我已尝试在IsLoading
中设置DataGridRow.Item
绑定,但尚无效。
DataGrid.RowStyle
另一种选择是将<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; }
// ...
}
中使用这些属性,但我还没有意识到如何使其正常工作。
答案 0 :(得分:1)
好的,所以你从服务器加载实体,但你仍然需要从ViewModel
访问该集合。让我们将此功能移至服务中。该服务允许您异步加载实体列表&#39; 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>
答案 1 :(得分:0)
您可以将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}。
答案 2 :(得分:0)
我决定使用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>