2014-01-07 168 views
0

我對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(); 

       } 

      } 
     } 

回答

5

您最初的嘗試讓您獲得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

相關問題