Liaison de données ObservableCollection nestede dans WPF

Je suis très nouveau sur WPF et j’essaie de créer une application d’auto-apprentissage à l’aide de WPF. J’ai du mal à comprendre des concepts tels que la liaison de données, les modèles de données, ItemControls dans sa totalité.

J’essaie de créer une page d’apprentissage avec les exigences suivantes à l’esprit.

1) La page peut comporter plusieurs questions. Une barre de défilement doit être affichée une fois que les questions remplissent toute la page. 2) Le format des choix varie en fonction du type de question. 3) l’utilisateur doit pouvoir sélectionner la réponse à la question.

Je suis confronté à un problème de liaison avec ObservableCollection nested et d’affichage du contenu conformément aux exigences ci-dessus.

Quelqu’un peut-il aider à créer une page comme indiqué ci-dessous et à utiliser INotifyPropertyChanged le long de XMAL pour créer la liaison nestede.

ma page

Voici le code de base que j’essaie d’utiliser pour afficher les questions et les réponses.

namespace Learn { public enum QuestionType { OppositeMeanings, LinkWords //todo } public class Question { public Question() { Choices = new ObservableCollection(); } public ssortingng Name { set; get; } public ssortingng Instruction { set; get; } public ssortingng Clue { set; get; } public ObservableCollection Choices { set; get; } public QuestionType Qtype { set; get; } public Answer Ans { set; get; } public int Marks { set; get; } } } namespace Learn { public class Choice { public ssortingng Name { get; set; } public bool isChecked { get; set; } } } namespace Learn { public class NestedItemsViewModel { public NestedItemsViewModel() { Questions = new ObservableCollection(); for (int i = 0; i < 10; i++) { Question qn = new Question(); qn.Name = "Qn" + i; for (int j = 0; j < 4; j++) { Choice ch = new Choice(); ch.Name = "Choice" + j; qn.Choices.Add(ch); } Questions.Add(qn); } } public ObservableCollection Questions { get; set; } } public partial class LearnPage : UserControl { public LearnPage() { InitializeComponent(); this.DataContext = new NestedItemsViewModel(); } } } 

Votre tentative initiale vous amène à 80% du chemin. J’espère que ma réponse vous rapprochera un peu.

Pour commencer, INotifyPropertyChanged est une interface prise en charge par un object pour informer le moteur Xaml que les données ont été modifiées et que l’interface utilisateur doit être mise à jour pour afficher la modification. Vous devez uniquement procéder de la sorte sur les propriétés clr standard.

Ainsi, si votre trafic de données est unidirectionnel, de l’interface utilisateur au modèle, vous n’avez pas besoin de mettre en œuvre INotifyPropertyChanged.

J’ai créé un exemple qui utilise le code que vous avez fourni, je l’ai modifié et créé une vue pour l’afficher. Le ViewModel et les classes de données sont les suivants: énumération publique QuestionType {OppositeMeanings, LinkWords}

 public class Instruction { public ssortingng Name { get; set; } public ObservableCollection Questions { get; set; } } public class Question : INotifyPropertyChanged { private Choice selectedChoice; private ssortingng instruction; public Question() { Choices = new ObservableCollection(); } public ssortingng Name { set; get; } public bool IsInstruction { get { return !ssortingng.IsNullOrEmpty(Instruction); } } public ssortingng Instruction { get { return instruction; } set { if (value != instruction) { instruction = value; OnPropertyChanged(); OnPropertyChanged("IsInstruction"); } } } public ssortingng Clue { set; get; } public ObservableCollection Choices { set; get; } public QuestionType Qtype { set; get; } public Choice SelectedChoice { get { return selectedChoice; } set { if (value != selectedChoice) { selectedChoice = value; OnPropertyChanged(); } } } public int Marks { set; get; } protected void OnPropertyChanged([CallerMemberName] ssortingng propertyName = null) { var handler = PropertyChanged; if (handler != null) { handler.Invoke(this, new PropertyChangedEventArgs(propertyName)); } } public event PropertyChangedEventHandler PropertyChanged; } public class Choice { public ssortingng Name { get; set; } public bool IsCorrect { get; set; } } public class NestedItemsViewModel { public NestedItemsViewModel() { Questions = new ObservableCollection(); for (var h = 0; h <= 1; h++) { Questions.Add(new Question() { Instruction = string.Format("Instruction {0}", h) }); for (int i = 1; i < 5; i++) { Question qn = new Question() { Name = "Qn" + ((4 * h) + i) }; for (int j = 0; j < 4; j++) { qn.Choices.Add(new Choice() { Name = "Choice" + j, IsCorrect = j == i - 1 }); } Questions.Add(qn); } } } public ObservableCollection Questions { get; set; } internal void SelectChoice(int questionIndex, int choiceIndex) { var question = this.Questions[questionIndex]; question.SelectedChoice = question.Choices[choiceIndex]; } } 

Notez que Answer a été remplacé par SelectedChoice. Ce n’est peut-être pas ce dont vous avez besoin, mais cela a rendu l’exemple un peu plus facile. J’ai également implémenté le modèle INotifyPropertyChanged sur SelectedChoice afin de pouvoir définir le SelectedChoice à partir du code ( notamment à partir d’un appel à SelectChoice ).

Le code de la fenêtre principale derrière instancie le ViewModel et gère un événement de bouton pour définir le choix du code derrière ( uniquement pour afficher le fonctionnement d’INotifyPropertyChanged ).

 public partial class MainWindow : Window { public MainWindow() { ViewModel = new NestedItemsViewModel(); InitializeComponent(); } public NestedItemsViewModel ViewModel { get; set; } private void ButtonBase_OnClick(object sender, RoutedEventArgs e) { ViewModel.SelectChoice(3, 3); } } 

Le Xaml est

                                     

Remarque: mon espace de noms learn est différent du vôtre. Par conséquent, si vous utilisez ce code, vous devrez le modifier pour votre espace de noms.

Ainsi, le ListBox primaire affiche une liste de questions. Chaque élément du ListBox (chaque question) est rendu à l’aide d’un DataTemplate. De même, dans le DataTemplate, un ListBox est utilisé pour afficher les choix et un DataTemplate est utilisé pour rendre chaque choix sous forme de bouton radio.

Points d’interêts.

  • Chaque choix est lié à la propriété IsSelected du ListBoxItem auquel il appartient. Il peut ne pas apparaître dans le xaml, mais il y aura un ListBoxItem pour chaque choix. La propriété IsSelected est conservée en synchronisation avec la propriété SelectedItem du ListBox (par le ListBox) et est liée au SelectedChoice dans votre question.
  • Le choix ListBox a un ItemsPanel. Cela vous permet d’utiliser la stratégie de disposition d’un type de panneau différent pour mettre en forme les éléments du ListBox. Dans ce cas, un StackPanel horizontal.
  • J’ai ajouté un bouton pour définir le choix de la question 3 sur 3 dans le modèle de vue. Cela vous montrera le travail INotifyPropertyChanged. Si vous supprimez l’appel OnPropertyChanged du créateur de la propriété SelectedChoice, la vue ne reflétera pas le changement.

L’exemple ci-dessus ne gère pas le type d’instruction.

Pour gérer les instructions, je voudrais soit

  1. Insérez l’instruction en tant que question et modifiez la question DataTemplate afin qu’elle n’affiche pas les choix pour une instruction; ou
  2. Créez une collection d’instructions dans le modèle de vue où le type d’instruction contient une collection de questions (le modèle de vue n’aurait plus de collection de questions).

La classe d’instruction serait quelque chose comme

 public class Instruction { public ssortingng Name { get; set; } public ObservableCollection Questions { get; set; } } 

Ajout basé sur un commentaire concernant l’expiration du minuteur et plusieurs pages.

Les commentaires ici visent à vous donner suffisamment d’informations pour savoir ce qu’il faut rechercher.

INotifyPropertyChanged

En cas de doute, implémentez INotifyPropertyChanged . Mon commentaire ci-dessus était de vous faire savoir pourquoi vous l’utilisez. Si des données déjà affichées sont manipulées à partir de code, vous devez implémenter INotifyPropertyChanged.

L’object ObservableCollection est génial pour gérer la manipulation de listes à partir de code. Non seulement implémente-t-il INotifyPropertyChanged, mais également INotifyCollectionChanged, ces deux interfaces garantissent que si la collection change, le moteur xaml le connaît et affiche les modifications. Notez que si vous modifiez une propriété d’un object de la collection, il vous appartiendra d’informer le moteur Xaml du changement en implémentant INotifyPropertyChanged sur l’object. ObservableCollection est génial, pas omnipercipient.

Pagination

Pour votre scénario, la pagination est simple. Stockez la liste complète des questions quelque part (mémoire, firebase database, fichier). Lorsque vous accédez à la page 1, interrogez le magasin pour ces questions et complétez ObservableCollection avec ces questions. Lorsque vous allez à la page 2, interrogez le magasin pour connaître les questions de la page 2, effacez ObservableCollection et effectuez un nouveau remplissage. Si vous instanciez ObservableCollection une fois, puis l’effacez et le repeupler lors de la pagination, l’actualisation ListBox sera traitée pour vous.

Minuteries

Les timers utilisent beaucoup de ressources du sharepoint vue de Windows et doivent donc être utilisées avec parcimonie. Vous pouvez utiliser un certain nombre de timers dans .net. J’ai tendance à jouer avec System.Threading.Timer ou System.Timers.Timer . Ces deux méthodes invoquent le rappel du minuteur sur un thread autre que DispatcherThread, ce qui vous permet de travailler sans affecter la réactivité de l’interface utilisateur. Toutefois, si pendant le travail, vous devez modifier l’interface utilisateur, vous aurez besoin de Dispatcher.Invoke ou de Dispatcher.BeginInvoke pour revenir au thread Dispatcher. BeginInvoke est asynchrone et par conséquent, ne doit pas suspendre le thread tant qu’il attend que DispatcherThread devienne inactif.

Ajout basé sur un commentaire concernant la séparation des modèles de données.

J’ai ajouté une IsInstruction à l’object Question (je n’ai pas implémenté de classe d’instruction). Cela montre un exemple de levée de l’événement PropertyChanged à partir de la propriété A (Instruction) pour la propriété B (IsInstruction).

J’ai déplacé le DataTemplate de la zone de liste vers le Window.Resources et lui ai donné une clé. J’ai également créé un deuxième DataTemplate pour les éléments d’instruction.

J’ai créé un DataTemplateSelector pour choisir le DataTemplate à utiliser. Les DataTemplateSelectors sont utiles lorsque vous devez sélectionner un DataTemplate pendant le chargement des données. Considérez cela comme un sélecteur OneTime. Si vous aviez besoin que le DataTemplate soit modifié pendant l’étendue des données qu’il restitue, utilisez un déclencheur. Le code pour le sélecteur est

 public class QuestionTemplateSelector : DataTemplateSelector { public override DataTemplate SelectTemplate(object item, DependencyObject container) { DataTemplate template = null; var question = item as Question; if (question != null) { template = question.IsInstruction ? InstructionTemplate : QuestionTemplate; if (template == null) { template = base.SelectTemplate(item, container); } } else { template = base.SelectTemplate(item, container); } return template; } public DataTemplate QuestionTemplate { get; set; } public DataTemplate InstructionTemplate { get; set; } } 

Le sélecteur est lié au ItemTemplateSelector du ItemsControl.

Enfin, j’ai converti le ListBox en ItemsControl. ItemsControl possède la plupart des fonctionnalités d’un ListBox (le contrôle ListBox est dérivé d’un ItemsControl), mais la fonctionnalité Selected manque. Vos questions ressembleront plus à une page de questions qu’à une liste.

REMARQUE: Bien que j’aie seulement ajouté le code du DataTemplateSelector à l’ajout, j’ai mis à jour les extraits de code dans le rest de la réponse pour fonctionner avec le nouveau DataTemplateSelector.

Ajout basé sur un commentaire concernant la définition du fond pour les bonnes et les mauvaises réponses

La définition dynamic de l’arrière-plan en fonction des valeurs du modèle nécessite un déclencheur, dans ce cas plusieurs déclencheurs.

J’ai mis à jour l’object Choice afin d’inclure un IsCorrect et lors de la création des questions dans le ViewModel, j’ai affecté IsCorrect à l’un des choix pour chaque réponse.

J’ai également mis à jour MainWindow pour inclure les déclencheurs de style sur RadioButton. Il y a quelques points à ne pas sur les déclencheurs 1. Le style ou le RadioButton définit le fond lorsque la souris est terminée. Un correctif nécessiterait de recréer le style de RadioButton. 1. Le déclencheur étant basé sur 2 valeurs, nous pouvons soit créer une autre propriété sur le modèle pour combiner les 2 propriétés, soit utiliser MultiBinding et un MultValueConverter. J’ai utilisé le MultiBinding et le MultiValueConverter est comme suit.

 public class SelectedItemIsCorrectToBooleanConverter : IMultiValueConverter { public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture) { var boolValues = values.OfType().ToList(); var isCorrectValue = boolValues[0]; var isSelected = boolValues[1]; if (isSelected) { return isCorrectValue; } return DependencyProperty.UnsetValue; } public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture) { throw new NotImplementedException(); } } 

J’espère que ça aide