J'ai la collection mettant en œuvre IList qui charge de manière asynchrone les données du serveur. Lorsque la collection est accédée par index, elle renvoie un objet stub et lance la demande de données en arrière-plan. Une fois la requête terminée, la collection actualise l'état interne et appelle l'événement PropertyChanged. Pour l'instant, les éléments retournés par la collection ressemblent à ceci:

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

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

Le problème est que je ne savais pas comment lier une telle collection à DataGrid. Je veux en quelque sorte définir le DataGrid.ItemsSource sur la collection de VirtualCollectionItem, faire DataGrid afficher les éléments réels (dans SelectedItem aussi) et laisser la possibilité d'utiliser des métadonnées (c'est-à-dire utiliser { {X5}} pour visualiser le chargement des données). J'ai essayé de définir la liaison DataGridRow.Item dans DataGrid.RowStyle mais cela n'a pas fonctionné.

<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>

Une autre option consiste à aplatir les propriétés VirtualCollectionItem en VirtualCollection lui-même:

class VirtualCollection
{
    // ...

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

    public IList<bool> IsLoadingItems { get; } 

    // ...
}

Et utilisez ces propriétés dans DataGrid, mais je ne sais pas comment faire fonctionner cela.

0
ilivit 21 avril 2017 à 16:38

3 réponses

Meilleure réponse

J'ai décidé d'envelopper le DataGrid avec le ViewModel:

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;
  }
}

Et liez-le au 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>
0
ilivit 26 avril 2017 à 07:00

Vous pouvez lier le VirtualCollection directement à la propriété DataGrid.ItemsSource. Ensuite, liez également la propriété SelectedItem:

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

Puis le 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();
        }
    }
}

Vous devez déclencher l'événement OnPropertyChanged après le chargement de la liste et vous devez le faire à partir de l'objet MyViewModel (ce que je pense que vous ne faites pas)! Vous pouvez également utiliser une ObservableCollection (vous pouvez l'étendre). Ensuite, vous n'avez pas besoin de l'événement OnPropertyChange. L'ajout / la suppression d'éléments de la collection avertit automatiquement l'interface utilisateur.

Le SelectedItem fonctionne indépendamment de la liste. Dans le DataTemplate pour une ligne, vous utiliserez {Binding IsLoading} et {Binding Item.SomeProperty}.

0
Dávid Molnár 21 avril 2017 à 14:23

Très bien, vous chargez donc les entités depuis le serveur, mais vous devez toujours accéder à la collection depuis ViewModel. Déplaçons cette fonctionnalité vers un service. Le service vous permet de charger de manière asynchrone une liste d'ID d'entités ou de charger les détails de l'entité particulière:

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 });
    }
}

La méthode GetPersonDetail renvoie un DTO:

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

    public int Id { get; set; }
}

Ce DTO peut être converti en un objet requis par votre ViewModel (et j'utilise Prism comme framework 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); }
    }
}

Vous pouvez convertir l'objet DTO en modèle comme ceci:

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;
    }
}

Et voici comment nous définissons les commandes (qui utilisent le service de récupération d'éléments) dans notre 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
}

Et enfin, le View peut être aussi simple que ceci:

<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
mechanic 21 avril 2017 à 20:05