Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
648 views
in Technique[技术] by (71.8m points)

c# - Nested ObservableCollection data binding in WPF

I am very new to WPF and trying to create a self learning application using WPF. I am struggling to understand concepts like data binding,data templates,ItemControls to a full extent.

I am trying to create a learn page with the following requirements in mind.

1) The page can have more than one question.A scroll bar should be displayed once the questions fills up the whole page. 2) The format of the choices vary based on the question type. 3) the user shall be able to select the answer for the question.

I am facing problem with binding nested ObservableCollection and displaying the content as for the above requirements.

Can someone help in how to create a page as shown below and how to use INotifyPropertyChanged along XMAL to do the nested binding.

myPage

Here is the basic code I am trying to use to use display the questions and answers.

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

                }

            }
        }
See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

You initial attempt gets you 80% of the way there. Hopefully, my answer will get you a little closer.

To start with, INotifyPropertyChanged is an interface an object supports to notify the Xaml engine that data has been modified and the user interface needs to be updated to show the change. You only need to do this on standard clr properties.

So if your data traffic is all one way, from the ui to the model, then there is no need for you to implement INotifyPropertyChanged.

I have created an example that uses the code you supplied, I have modified it and created a view to display it. The ViewModel and the data classes are as follows public enum 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];
    }
}

Notice that Answer has been changed to a SelectedChoice. This may not be what you require but it made the example a little easier. i have also implemented the INotifyPropertyChanged pattern on the SelectedChoice so I can set the SelectedChoice from code (notably from a call to SelectChoice).

The main windows code behind instantiates the ViewModel and handles a button event to set a choice from code behind (purely to show INotifyPropertyChanged working).

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

The Xaml is

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

Note: My learn namespace is different from yours so if you use this code, you will need to modify it to your namespace.

So, the primary ListBox display a list of Questions. Each item in the ListBox (each Question) is rendered using a DataTemplate. Similarly, in the DataTemplate, a ListBox is used to display the choices and a DataTemplate is used to render each choice as a radio button.

Points of interest.

  • Each choice is bound to the IsSelected property of the ListBoxItem it belongs to. It may not appear in the xaml but there will be a ListBoxItem for each choice. The IsSelected property is kept in sync with the SelectedItem property of the ListBox (by the ListBox) and that is bound to the SelectedChoice in your question.
  • The choice ListBox has an ItemsPanel. This allows you to use the layout strategy of a different type of panel to layout the items of the ListBox. In this case, a horizontal StackPanel.
  • I have added a button to set the choice of question 3 to 3 in the viewmodel. This will show you INotifyPropertyChanged working. If you remove the OnPropertyChanged call from the setter of the SelectedChoice property, the view will not reflect the change.

The example above does not handle the Instruction Type.

To handle instructions, I would either

  1. Insert the instruction as a question and change the question DataTemplate so it does not display the choices for an instruction; or
  2. Create a collection of Instructions in the view model where the Instruction type has a collection of questions (the view model would no longer have a collection of questions).

The Instruction class would be something like

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

Addition based on comment regarding timer expiration and multiple pages.

The comments here are aimed at giving you enough information to know what to search for.

INotifyPropertyChanged

If in doubt, implement INotifyPropertyChanged. My comment above was to let you know why you use it. If you have data already displayed that will be manipulated from code, then you must implement INotifyPropertyChanged.

The ObservableCollection object is awesome for handling the manipulation of lists from code. Not only does it implement INotifyPropertyChanged, but it also implements INotifyCollectionChanged, both of these interfaces ensure that if the collection changes, the xaml engine knows about it and displays the changes. Note that if you modify a property of an object in the collection, it will be up to you to notify the Xaml engine of the change by implementing INotifyPropertyChanged on the object. The ObservableCollection is awesome, not omnipercipient.

Paging

For your scenario, paging is simple. Store the complete list of questions somewhere (memory, database, file). When you go to page 1, query the store for those questions and populate the ObservableCollection with those questions. When you go to page 2, query the store for page 2 questions, CLEAR the ObservableCollection and re populate. If you instantiate the ObservableCollection once and then clear and repopulate it while paging, the ListBox refresh will be handled for you.

Timers<


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
...