2009-05-17 3 views
3

J'ai une classe qui a deux ObservableCollection < TimeValue>, où TimeValue est un appariement DateTime/Valeur personnalisé avec notification de changement (via INotifyPropertyChanged). J'appelle ces cibles et réels.WPF DataGrid - Combinaison de TimeSeries avec MultiBinding, perte de notification de modification. Pourquoi?

Lorsque je les lie à un graphique, tout fonctionne parfaitement, et j'ai deux LineSeries. Si je lie l'un d'entre eux à un DataGrid, avec une colonne pour "Date" et une colonne pour "Value", fonctionne parfaitement à nouveau. Je reçois même la liaison TwoWay dont j'ai besoin.

Cependant, j'ai besoin d'un DataGrid avec une colonne "Date" et une colonne pour les cibles et les réels. Le problème est que j'ai besoin de lister TOUTES les dates dans une plage, alors que certaines de ces dates peuvent ne pas avoir de valeurs correspondantes dans les cibles, les réels, ou les deux. Donc, j'ai décidé de faire un MultiBinding qui prend les cibles et les réels en entrée, et produit une TimeSeriesC combinée, avec des valeurs nulles quand l'un des originaux n'a aucune valeur.

Cela fonctionne, mais ne répond à aucun changement dans les données sous-jacentes.

Cela fonctionne bien (se liant à un ObservableCollection):

<ctrls:DataGrid Grid.Row="1" Height="400" AutoGenerateColumns="False" CanUserDeleteRows="False" SelectionUnit="Cell"> 
<ctrls:DataGrid.ItemsSource> 
    <Binding Path="Targets"/> 
    <!--<MultiBinding Converter="{StaticResource TargetActualListConverter}"> 
     <Binding Path="Targets"/> 
     <Binding Path="Actuals"/> 
    </MultiBinding>--> 
</ctrls:DataGrid.ItemsSource> 
<ctrls:DataGrid.Columns> 
    <ctrls:DataGridTextColumn Header="Date" Binding="{Binding Date,StringFormat={}{0:ddd, MMM d}}"/> 
    <ctrls:DataGridTextColumn Header="Target" Binding="{Binding Value}"/> 
    <!--<ctrls:DataGridTextColumn Header="Target" Binding="{Binding Value[0]}"/> 
    <ctrls:DataGridTextColumn Header="Actual" Binding="{Binding Value[1]}"/>--> 
</ctrls:DataGrid.Columns> 

Cela fonctionne, mais seulement lors de la première initialisation. Aucune réponse au changement de notification:

<ctrls:DataGrid Grid.Row="1" Height="400" AutoGenerateColumns="False" CanUserDeleteRows="False" SelectionUnit="Cell"> 
<ctrls:DataGrid.ItemsSource> 
    <!--<Binding Path="Targets"/>--> 
    <MultiBinding Converter="{StaticResource TargetActualListConverter}"> 
     <Binding Path="Targets"/> 
     <Binding Path="Actuals"/> 
    </MultiBinding> 
</ctrls:DataGrid.ItemsSource> 
<ctrls:DataGrid.Columns> 
    <ctrls:DataGridTextColumn Header="Date" Binding="{Binding Date,StringFormat={}{0:ddd, MMM d}}"/> 
    <!--<ctrls:DataGridTextColumn Header="Target" Binding="{Binding Value}"/>--> 
    <ctrls:DataGridTextColumn Header="Target" Binding="{Binding Value[0]}"/> 
    <ctrls:DataGridTextColumn Header="Actual" Binding="{Binding Value[1]}"/> 
</ctrls:DataGrid.Columns> 

Et voici mon IMultiValueConverter:

class TargetActualListConverter : IMultiValueConverter 
{ 
    public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture) 
    { 
     TimeSeries<double> Targets = values[0] as TimeSeries<double>; 
     TimeSeries<double> Actuals = values[1] as TimeSeries<double>; 
     DateTime[] range = TimeSeries<double>.GetDateRange(Targets, Actuals);//Get min and max Dates 
     int count = (range[1] - range[0]).Days;//total number of days 
     DateTime currDate = new DateTime(); 
     TimeSeries<double?[]> combined = new TimeSeries<double?[]>(); 
     for (int i = 0; i < count; i++) 
     { 
      currDate = range[0].AddDays(i); 
      double?[] vals = { Targets.Dates.Contains(currDate) ? (double?)Targets.GetValueByDate(currDate) : null, Actuals.Dates.Contains(currDate) ? (double?)Actuals.GetValueByDate(currDate) : null }; 
      combined.Add(new TimeValue<double?[]>(currDate, vals)); 
     } 
     return combined; 
    } 

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture) 
    { 
     TimeSeries<double?[]> combined = value as TimeSeries<double?[]>; 
     TimeSeries<double> Targets = new TimeSeries<double>(); 
     TimeSeries<double> Actuals = new TimeSeries<double>(); 

     foreach (TimeValue<double?[]> tv in combined) 
     { 
      if(tv.Value[0]!=null) 
       Targets.Add(new TimeValue<double>(tv.Date,(double)tv.Value[0])); 
      if (tv.Value[1] != null) 
       Actuals.Add(new TimeValue<double>(tv.Date, (double)tv.Value[1])); 
     } 
     TimeSeries<double>[] result = { Targets, Actuals }; 
     return result; 

    } 
} 

Je ne peux pas être trop loin, car il affiche les valeurs.

Qu'est-ce que je fais mal? Ou, alternativement, y a-t-il une manière plus facile de faire ceci?

Merci à tous!

Répondre

2

Cela semble provenir du convertisseur. ObservableCollection implémente INotifyCollectionChanged, qui notifie l'interface utilisateur lorsqu'il y a un changement dans la collection (Ajouter/Supprimer/Remplacer/Déplacer/Réinitialiser). Ce sont toutes des modifications apportées à la collection, pas le contenu de la collection, et les mises à jour que vous avez vues étaient dues à l'implémentation de votre classe par INotifyPropertyChanged. Étant donné que MultiCoverter renvoie une nouvelle collection de nouveaux objets, les données dans les collections initiales ne seront pas propagées à ceux-ci, car il n'y a aucune liaison aux objets d'origine à notifier.

La première chose que je suggère est de jeter un oeil à l'élément CompositeCollection et de voir si cela va répondre à vos besoins.

Au lieu de définir la ItemsSource que vous êtes, vous pouvez conserver les objets originaux avec quelque chose comme:

<ctrls:DataGrid.ItemsSource> 
    <CompositeCollection> 
      <CollectionContainer Collection="{Binding Targets}" /> 
      <CollectionContainer Collection="{Binding Actuals}" /> 
     </CompositeCollection> 
</ctrls:DataGrid.ItemsSource> 

(je suppose « ne répond pas à tout changement dans les données sous-jacente » fait référence à l'évolution les valeurs, ne modifiant pas la collection, si je me trompe, faites le moi savoir et je l'examinerai de plus près.)

Modifier ajouts
Dans le cas où cela ne fonctionne pas une alternative est d'écrire une nouvelle classe qui se terminera à la fois la cible et collections réelles. Ensuite, une seule ObservableCollection peut être créée en utilisant ces wrappers. C'est en fait une meilleure méthode sur l'utilisation d'un ValueConverter ou en utilisant un CompositeCollection. Avec soit vous perdez une partie de la fonctionnalité qui était à l'origine présente. En utilisant un convertisseur de valeur pour recréer une collection, il n'est plus lié directement aux objets d'origine et la notification de propriété peut être perdue. En utilisant CompositeCollection, vous n'avez plus qu'une seule collection qui peut être itérée ou modifiée avec add/delete/move etc, car elle doit savoir sur quelle collection opérer.

Ce type de fonctionnalité de wrapping peut être très utile dans WPF, et est une version très simplifiée d'un ViewModel, une partie du modèle de conception M-V-VM. Il peut être utilisé lorsque vous n'avez pas accès aux classes sous-jacentes pour ajouter INotifyPropertyChanged ou IDataErrorInfo et peut également vous aider à ajouter des fonctionnalités supplémentaires telles que l'état et l'interaction aux modèles sous-jacents.

Voici un court exemple démontrant cette fonctionnalité où nos deux classes initiales ont la même propriété Name et n'implémentent pas INotifyPropertyChanged qui n'est pas partagé entre elles.

public partial class Window1 : Window 
{ 
    public Window1() 
    { 
     InitializeComponent(); 

     Foo foo1 = new Foo { ID = 1, Name = "Foo1" }; 
     Foo foo3 = new Foo { ID = 3, Name = "Foo3" }; 
     Foo foo5 = new Foo { ID = 5, Name = "Foo5" }; 
     Bar bar1 = new Bar { ID = 1, Name = "Bar1" }; 
     Bar bar2 = new Bar { ID = 2, Name = "Bar2" }; 
     Bar bar4 = new Bar { ID = 4, Name = "Bar4" }; 

     ObservableCollection<FooBarViewModel> fooBar = new ObservableCollection<FooBarViewModel>(); 
     fooBar.Add(new FooBarViewModel(foo1, bar1)); 
     fooBar.Add(new FooBarViewModel(bar2)); 
     fooBar.Add(new FooBarViewModel(foo3)); 
     fooBar.Add(new FooBarViewModel(bar4)); 
     fooBar.Add(new FooBarViewModel(foo5)); 

     this.DataContext = fooBar; 
    } 
} 

public class Foo 
{ 
    public int ID { get; set; } 
    public string Name { get; set; } 
} 

public class Bar 
{ 
    public int ID { get; set; } 
    public string Name { get; set; } 
} 

public class FooBarViewModel : INotifyPropertyChanged 
{ 
    public Foo WrappedFoo { get; private set; } 
    public Bar WrappedBar { get; private set; } 

    public int ID 
    { 
     get 
     { 
      if (WrappedFoo != null) 
      { return WrappedFoo.ID; } 
      else if (WrappedBar != null) 
      { return WrappedBar.ID; } 
      else 
      { return -1; } 
     } 
     set 
     { 
      if (WrappedFoo != null) 
      { WrappedFoo.ID = value; } 
      if (WrappedBar != null) 
      { WrappedBar.ID = value; } 

      this.NotifyPropertyChanged("ID"); 
     } 
    } 

    public string BarName 
    { 
     get 
     { 
      return WrappedBar.Name; 
     } 
     set 
     { 
      WrappedBar.Name = value; 
      this.NotifyPropertyChanged("BarName"); 
     } 
    } 

    public string FooName 
    { 
     get 
     { 
      return WrappedFoo.Name; 
     } 
     set 
     { 
      WrappedFoo.Name = value; 
      this.NotifyPropertyChanged("FooName"); 
     } 
    } 

    public FooBarViewModel(Foo foo) 
     : this(foo, null) { } 
    public FooBarViewModel(Bar bar) 
     : this(null, bar) { } 
    public FooBarViewModel(Foo foo, Bar bar) 
    { 
     WrappedFoo = foo; 
     WrappedBar = bar; 
    } 

    public event PropertyChangedEventHandler PropertyChanged; 

    private void NotifyPropertyChanged(String info) 
    { 
     if (PropertyChanged != null) 
     { 
      PropertyChanged(this, new PropertyChangedEventArgs(info)); 
     } 
    } 
} 

Et puis dans la fenêtre:

<ListView ItemsSource="{Binding}"> 
    <ListView.View> 
     <GridView> 
      <GridViewColumn Header="ID" DisplayMemberBinding="{Binding ID}"/> 
      <GridViewColumn Header="Foo Name" DisplayMemberBinding="{Binding FooName}"/> 
      <GridViewColumn Header="Bar Name" DisplayMemberBinding="{Binding BarName}"/> 
     </GridView> 
    </ListView.View> 
</ListView> 
+1

Merci rmoore. Malheureusement, la configuration que j'ai ne répond même pas aux événements CollectionChanged. J'ai essayé w/CompositeCollections, mais depuis la collection que je veux afficher n'est même pas de la même taille que les deux que j'ai entré, je ne vois pas comment je le ferais fonctionner. Bizarrement, la méthode Convert n'est appelée qu'une seule fois (par valeur), lors de l'affichage initial, et plus jamais. – AdrianoFerrari

+1

Dans ce cas, le moyen le plus simple de résoudre ceci sans en savoir plus serait de créer une sorte de classe wrapper qui peut fusionner une cible et/ou un réel. Ensuite, si vous créez une seule ObservableCollection de ces wrappers, il devrait être beaucoup plus facile de lier. – rmoore

+0

Je suppose que je vais devoir le faire de cette façon. Je posterai ici si je réussis à le faire comme je le souhaitais, car cela aurait été plus simple. Merci encore rmoore. Y at-il de toute façon pour vous attribuer des points sans marquer cela comme étant répondu? – AdrianoFerrari

Questions connexes