WPF中的嵌套ObservableCollection数据绑定

问题描述:

我对WPF非常陌生,并试图使用WPF创建自学习应用程序。 我很努力地理解像数据绑定,数据模板,ItemControls这样的概念。WPF中的嵌套ObservableCollection数据绑定

我想创建一个学习页面,并考虑到以下要求。

1)页面可以有多个问题。一旦 问题填满了整个页面,应该会显示一个滚动条。 2)选择的格式因问题类型而异。 3)用户应能够选择问题的答案。

我面临着绑定嵌套的ObservableCollection和显示内容的问题,以上述要求。

有人可以帮助如何创建如下所示的页面,以及如何沿着XMAL使用INotifyPropertyChanged来执行嵌套绑定。

myPage

这里是我想用就用显示的问题和答案的基本代码。

 namespace Learn 
     { 
      public enum QuestionType 
      { 
       OppositeMeanings, 
       LinkWords 
       //todo 
      } 
      public class Question 
      { 
       public Question() 
       { 
        Choices = new ObservableCollection<Choice>(); 

       } 
       public string Name { set; get; } 
       public string Instruction { set; get; } 
       public string Clue { set; get; } 
       public ObservableCollection<Choice> Choices { set; get; } 
       public QuestionType Qtype { set; get; } 
       public Answer Ans { set; get; } 
       public int Marks { set; get; } 
      } 
     } 

     namespace Learn 
     { 
      public class Choice 
      { 
       public string Name { get; set; } 
       public bool isChecked { get; set; } 
      } 
     } 
     namespace Learn 
     { 
      public class NestedItemsViewModel 
      { 
       public NestedItemsViewModel() 
       { 
        Questions = new ObservableCollection<Question>(); 
        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<Question> Questions { get; set; } 
      } 

      public partial class LearnPage : UserControl 
      { 

       public LearnPage() 
       { 
        InitializeComponent(); 

        this.DataContext = new NestedItemsViewModel(); 

       } 

      } 
     } 

您最初的尝试让您获得80%的出路。希望我的回答能让你更接近一点。

首先,INotifyPropertyChanged的一个目的是支持通知的Xaml引擎,数据已被修改,并且用户接口需要被更新以显示的变化的接口。你只需要在标准的clr属性上做到这一点。

所以,如果你的数据流量是单向的,从UI到模型,那么就没有必要让你执行INotifyPropertyChanged。

我创建了一个使用您提供的代码的示例,我修改了它并创建了一个视图来显示它。是视图模型和数据类如下 公共枚举QuestionType { OppositeMeanings, LinkWords }

public class Instruction 
{ 
    public string Name { get; set; } 
    public ObservableCollection<Question> Questions { get; set; } 
} 

public class Question : INotifyPropertyChanged 
{ 
    private Choice selectedChoice; 
    private string instruction; 

    public Question() 
    { 
     Choices = new ObservableCollection<Choice>(); 

    } 
    public string Name { set; get; } 
    public bool IsInstruction { get { return !string.IsNullOrEmpty(Instruction); } } 
    public string Instruction 
    { 
     get { return instruction; } 
     set 
     { 
      if (value != instruction) 
      { 
       instruction = value; 
       OnPropertyChanged(); 
       OnPropertyChanged("IsInstruction"); 
      } 
     } 
    } 
    public string Clue { set; get; } 
    public ObservableCollection<Choice> 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] string propertyName = null) 
    { 
     var handler = PropertyChanged; 
     if (handler != null) 
     { 
      handler.Invoke(this, new PropertyChangedEventArgs(propertyName)); 
     } 
    } 
    public event PropertyChangedEventHandler PropertyChanged; 
} 

public class Choice 
{ 
    public string Name { get; set; } 
    public bool IsCorrect { get; set; } 
} 

public class NestedItemsViewModel 
{ 
    public NestedItemsViewModel() 
    { 
     Questions = new ObservableCollection<Question>(); 
     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<Question> Questions { get; set; } 

    internal void SelectChoice(int questionIndex, int choiceIndex) 
    { 
     var question = this.Questions[questionIndex]; 
     question.SelectedChoice = question.Choices[choiceIndex]; 
    } 
} 

注意这个问题的答案已经被更改为SelectedChoice。这可能不是你所需要的,但它使得这个例子更容易一些。我还实施在SelectedChoice的INotifyPropertyChanged的图案,所以我可以(从呼叫特别是向SelectChoice)设置从代码SelectedChoice。

后面的主窗口代码实例化ViewModel并处理按钮事件以设置代码后面的选项(纯粹显示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); 
    } 
} 

的XAML是

<Window x:Class="StackOverflow._20984156.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
     xmlns:learn="clr-namespace:StackOverflow._20984156" 
     DataContext="{Binding RelativeSource={RelativeSource Self}, Path=ViewModel}" 
     Title="MainWindow" Height="350" Width="525"> 
    <Window.Resources> 

     <learn:SelectedItemIsCorrectToBooleanConverter x:Key="SelectedCheckedToBoolean" /> 

     <Style x:Key="ChoiceRadioButtonStyle" TargetType="{x:Type RadioButton}" BasedOn="{StaticResource {x:Type RadioButton}}"> 
      <Style.Triggers> 
       <DataTrigger Value="True"> 
        <DataTrigger.Binding> 
         <MultiBinding Converter="{StaticResource SelectedCheckedToBoolean}"> 
          <Binding Path="IsCorrect" /> 
          <Binding RelativeSource="{RelativeSource Self}" Path="IsChecked" /> 
         </MultiBinding> 
        </DataTrigger.Binding> 
        <Setter Property="Background" Value="Green"></Setter> 
       </DataTrigger> 
       <DataTrigger Value="False"> 
        <DataTrigger.Binding> 
         <MultiBinding Converter="{StaticResource SelectedCheckedToBoolean}"> 
          <Binding Path="IsCorrect" /> 
          <Binding RelativeSource="{RelativeSource Self}" Path="IsChecked" /> 
         </MultiBinding> 
        </DataTrigger.Binding> 
        <Setter Property="Background" Value="Red"></Setter> 
       </DataTrigger> 
      </Style.Triggers> 
     </Style> 

     <DataTemplate x:Key="InstructionTemplate" DataType="{x:Type learn:Question}"> 
      <TextBlock Text="{Binding Path=Instruction}" /> 
     </DataTemplate> 

     <DataTemplate x:Key="QuestionTemplate" DataType="{x:Type learn:Question}"> 
      <StackPanel Margin="10 0"> 
       <TextBlock Text="{Binding Path=Name}" /> 
       <ListBox ItemsSource="{Binding Path=Choices}" SelectedItem="{Binding Path=SelectedChoice}" HorizontalAlignment="Stretch"> 
        <ListBox.ItemsPanel> 
         <ItemsPanelTemplate> 
          <StackPanel Orientation="Horizontal" /> 
         </ItemsPanelTemplate> 
        </ListBox.ItemsPanel> 
        <ListBox.ItemTemplate> 
         <DataTemplate DataType="{x:Type learn:Choice}"> 
          <RadioButton Content="{Binding Path=Name}" IsChecked="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ListBoxItem}}, Path=IsSelected}" Margin="10 1" 
             Style="{StaticResource ChoiceRadioButtonStyle}" /> 
         </DataTemplate> 
        </ListBox.ItemTemplate> 
       </ListBox> 
      </StackPanel> 
     </DataTemplate> 
    </Window.Resources> 

    <DockPanel> 
     <StackPanel Orientation="Horizontal" DockPanel.Dock="Bottom"> 
      <Button Content="Select Question 3 choice 3" Click="ButtonBase_OnClick" /> 
     </StackPanel> 
     <ItemsControl ItemsSource="{Binding Path=Questions}"> 
      <ItemsControl.ItemTemplateSelector> 
       <learn:QuestionTemplateSelector QuestionTemplate="{StaticResource QuestionTemplate}" InstructionTemplate="{StaticResource InstructionTemplate}" /> 
      </ItemsControl.ItemTemplateSelector> 
     </ItemsControl> 
    </DockPanel> 
</Window> 

注:我学会命名与你的不同,所以如果您使用此代码,您需要将其修改为您的命名空间。

因此,主要ListBox显示问题列表。 ListBox中的每个项目(每个问题)都使用DataTemplate呈现。同样,在DataTemplate中,使用ListBox显示选项,并使用DataTemplate将每个选项呈现为单选按钮。

兴趣点。

  • 每个选项都绑定到它所属的ListBoxItem的IsSelected属性。它可能不会出现在xaml中,但每个选项都会有一个ListBoxItem。 IsSelected属性保持与ListBox的SelectedItem属性(通过ListBox)保持同步,并且在您的问题中绑定到SelectedChoice。
  • 选择ListBox有一个ItemsPanel。这使您可以使用不同类型面板的布局策略来布置ListBox的项目。在这种情况下,一个水平的StackPanel。
  • 我添加了一个按钮来设置viewmodel中问题3到3的选择。这会告诉你INotifyPropertyChanged的工作。如果您从SelectedChoice属性的setter中删除OnPropertyChanged调用,该视图将不会反映该更改。

上述示例不处理指令类型。

要处理的指示,我要么

  1. 插入指令的问题,改变问题的DataTemplate因此它不会显示一个指令的选择;或
  2. 在视图模型中创建指令集合,其中指令类型包含一组问题(视图模型不再包含问题集合)。

指令类会是这样的基础上评论有关计时器到期和多页

public class Instruction 
{ 
    public string Name { get; set; } 
    public ObservableCollection<Question> Questions { get; set; } 
} 

加成。

此处的评论旨在为您提供足够的信息来了解要搜索的内容。

INotifyPropertyChanged的

如果有疑问,执行INotifyPropertyChanged。我上面的评论是让你知道你为什么使用它。如果您已经显示了将从代码操作的数据,那么您必须实现INotifyPropertyChanged。

ObservableCollection对象非常适合处理代码中列表的操作。它不仅实现INotifyPropertyChanged,而且还实现INotifyCollectionChanged,这两个接口都确保如果集合更改,xaml引擎就会知道它并显示更改。请注意,如果您修改集合中某个对象的属性,则需要通过在对象上实现INotifyPropertyChanged来通知Xaml引擎该更改。 ObservableCollection非常棒,不是全方位的。

寻呼

对于您的情况,分页很简单。在某处存储完整的问题列表(内存,数据库,文件)。当您转到第1页时,请向商店查询这些问题,并用这些问题填充ObservableCollection。当您转到第2页时,请查询商店中的第2页问题,清除ObservableCollection并重新填充。如果实例化ObservableCollection一次,然后在分页时清除并重新填充它,则将为您处理ListBox刷新。

定时器

计时器是相当的资源从一个窗口点密集,因此,应谨慎使用。您可以使用.net中的多个计时器。我倾向于使用System.Threading.TimerSystem.Timers.Timer。这两种方法都会调用除DispatcherThread之外的线程的计时器回调,这样可以在不影响UI响应的情况下进行工作。但是,如果在工作过程中需要修改UI,则需要Dispatcher.InvokeDispatcher.BeginInvoke才能返回分派器线程。 BeginInvoke是异步的,因此在等待DispatcherThread空闲时不应挂起线程。

基于关于分离数据模板的评论而添加。

我在Question对象中添加了一个IsInstruction(我没有实现一个Instruction类)。这显示了从属性B(IsInstruction)的属性A(指令)引发PropertyChanged事件的示例。

我将DataTemplate从列表框移动到Window.Resources并给它一个键。我还为指令项目创建了第二个DataTemplate。

我创建了一个DataTemplateSelector来选择使用哪个DataTemplate。当您正在加载数据时需要选择DataTemplate时,DataTemplateSelectors很好。考虑它一个OneTime选择器。如果您要求DataTemplate在渲染的数据范围内进行更改,则应使用触发器。选择器的代码是

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

选择器绑定到ItemsControl的ItemTemplateSelector。

最后,我将ListBox转换为ItemsControl。 ItemsControl具有ListBox的大部分功能(ListBox控件是从ItemsControl派生的),但它缺少Selected功能。它会让你的问题看起来更像是一个问题而不是一个列表。

注意:尽管我只是将DataTemplateSelector的代码添加到了附加内容中,但我更新了代码片段,其余的答案都与新的DataTemplateSelector一起使用。根据有关设置背景正确和错误的答案

设置动态基于模型值的背景评论

加法需要一个触发,在这种情况下,多个触发器。

我已经更新了Choice对象以包含IsCorrect,并且在创建ViewModel中的问题时,我已经为每个答案的其中一个选项分配了IsCorrect。

我也更新了MainWindow以在RadioButton中包含样式触发器。有几点不是关于触发器 1.样式或RadioButton在鼠标结束时设置背景。修复将需要重新创建RadioButton的样式。 1.由于触发器基于2个值,我们可以在模型上创建另一个属性来组合2个属性,或使用MultiBinding和MultValueConverter。我已经使用了MultiBindingMultiValueConverter如下。

public class SelectedItemIsCorrectToBooleanConverter : IMultiValueConverter 
{ 
    public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture) 
    { 
     var boolValues = values.OfType<bool>().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(); 
    } 
} 

我希望这有助于

+0

谢谢Mark.This肯定helps.I想一旦计时器到期禁用的选择,如果用户选择下一个我想刷新页面,并显示新的问题和选择。在这种情况下,你不认为需要INotifyPropertyChanged?请让我知道你的想法。 – Ven

+0

谢谢马克...现在我可以轻松实现我的应用程序:) – Ven

+0

马克..它的工作..谢谢你..但我怎么做一个单独的数据模板为每个指令,如上图所示(即问题并且在特定指令类型下的选择应该有一个数据模板,在另一个指令类型下的问题和选择应该具有不同的数据模板) – Ven