WPF中嵌套的ObservableCollection数据绑定

Ven*_*Ven 1 c# silverlight wpf

我是WPF的新手并尝试使用WPF创建自学习应用程序.我正在努力理解数据绑定,数据模板,ItemControls等概念.

我正在尝试创建一个包含以下要求的学习页面.

1)页面可以有多个问题.一旦问题填满整个页面,就会显示一个滚动条.2)选择的格式根据问题类型而有所不同.3)用户应能够选择问题的答案.

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

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

我的页面

这是我试图用来显示问题和答案的基本代码.

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

                }

            }
        }
Run Code Online (Sandbox Code Playgroud)

Mar*_*vis 5

你最初的尝试可以让你获得80%的胜利.希望我的答案会让你更接近.

首先,INotifyPropertyChanged是一个对象支持的接口,用于通知Xaml引擎数据已被修改,并且需要更新用户界面以显示更改.您只需要在标准clr属性上执行此操作.

因此,如果您的数据流量都是单向的,从ui到模型,则无需实现INotifyPropertyChanged.

我创建了一个使用您提供的代码的示例,我修改了它并创建了一个显示它的视图.ViewModel和数据类如下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];
    }
}
Run Code Online (Sandbox Code Playgroud)

请注意,答案已更改为SelectedChoice.这可能不是您所需要的,但它使示例更容易一些.我还在SelectedChoice上实现了INotifyPropertyChanged模式,因此我可以从代码中设置SelectedChoice(特别是调用SelectChoice).

后面的主要Windows代码实例化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);
    }
}
Run Code Online (Sandbox Code Playgroud)

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>
Run Code Online (Sandbox Code Playgroud)

注意:我的学习命名空间与您的命名空间不同,因此如果您使用此代码,则需要将其修改为命名空间.

因此,主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; }
}
Run Code Online (Sandbox Code Playgroud)

添加基于有关计时器到期和多页的评论.

这里的评论旨在为您提供足够的信息,以了解要搜索的内容.

INotifyPropertyChanged的

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

ObservableCollection对象非常适合处理代码中的列表操作.它不仅实现了INotifyPropertyChanged,而且还实现了INotifyCollectionChanged,这两个接口都确保如果集合发生变化,xaml引擎会知道它并显示更改.请注意,如果修改集合中对象的属性,则可以通过在对象上实现INotifyPropertyChanged来通知Xaml引擎.ObservableCollection很棒,而不是omnipercipient.

分页

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

计时器

从Windows的角度来看,定时器是非常耗费资源的,因此应该谨慎使用.您可以使用.net中的许多计时器.我倾向于使用System.Threading.TimerSystem.Timers.Timer.这两个都在DispatcherThread之外的线程上调用计时器回调,这允许您在不影响UI响应的情况下执行工作.但是,如果在工作期间需要修改UI,则需要使用Dispatcher.InvokeDispatcher.BeginInvoke来返回Dispatcher线程.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; }
}
Run Code Online (Sandbox Code Playgroud)

选择器绑定到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.我使用了MultiBinding,MultiValueConverter如下所示.

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();
    }
}
Run Code Online (Sandbox Code Playgroud)

我希望这有帮助

  • 有两种方式.首先,您需要创建DataTemplate资源并为其指定名称(键),一个用于指令,一个用于提问.在第一个解决方案中,您创建ItemsContainerStyle并基于数据,使用触发器来设置ContentTemplate.第二种解决方案是创建DataTemplateSelector.我更新了答案,使用DataTemplateSelector显示DataTemplates与问题​​和说明的分离. (2认同)
  • 嘿Ven,你可以通过使用触发器来改变背景颜色.创建要为其设置背景的控件样式.在样式中创建DataTrigger并设置Binding属性(标准绑定表达式)和值.当触发器为真时,将应用触发器内的设置器.当触发器为false时,将删除setter并应用默认设置器.请注意,RadioButton的背景位于圆圈内.我已经用必要的代码和描述提升了答案. (2认同)
  • 如果模板或样式在控件的生命周期内可以更改,我会使用触发器.我会使用DataTemplateSelector if; a - 创建控件时选择模板,该模板用于控件的生命周期;**和**b - 如果控件支持DataTemplate选择器. (2认同)