如何在 XAML 中构建问卷 UserControl,其问题具有相同的答案?

How can I build a questionnaire UserControl in XAML whose questions share the same answers?

我想构建一个 UserControl 来表示问卷,如下图所示(忽略缺少样式)。

我希望能够指定XAML中的重要内容,比如

<local:QuestionnaireControl>
  <local:QuestionnaireControl.Questions>
    <local:QuestionAndAnswers Number="1" Question="Is this working?" />
    <local:QuestionAndAnswers Number="2" Question="Are these questions sharing answers?" />
  </local:QuestionnaireControl.Questions>
  <local:QuestionnaireControl.Answers>
    <local:Answer Value="0" Text="Yes" />
    <local:Answer Value="1" Text="No" />
    <local:Answer Value="2" Text="Help Me Please" />
  </local:QuestionnaireControl.Answers>
</local:QuestionnaireControl>

所以我有以下QuestionnaireControl.xaml

<UserControl x:Class="MyProject.QuestionnaireControl"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             mc:Ignorable="d"
             xmlns:local="clr-namespace:MyProject"
             DataContext="{Binding RelativeSource={RelativeSource Self}}"
             d:DataContext="{d:DesignInstance Type=local:QuestionnaireControl, IsDesignTimeCreatable=True}">
    <ItemsControl ItemsSource="{Binding Questions}">
        <ItemsControl.ItemsPanel>
            <ItemsPanelTemplate>
                <StackPanel Orientation="Vertical"/>
            </ItemsPanelTemplate>
        </ItemsControl.ItemsPanel>
        <ItemsControl.ItemTemplate>
            <DataTemplate>
                <StackPanel Orientation="Horizontal">
                    <TextBlock Text="{Binding Number, StringFormat='{}{0}.'}" Margin="0,0,10,0" />
                    <TextBlock Text="{Binding Question}" Width="220"/>
                    <ItemsControl ItemsSource="{Binding Answers}">
                        <ItemsControl.ItemsPanel>
                            <ItemsPanelTemplate>
                                <StackPanel Orientation="Horizontal"/>
                            </ItemsPanelTemplate>
                        </ItemsControl.ItemsPanel>
                        <ItemsControl.ItemTemplate>
                            <DataTemplate>
                                <StackPanel Orientation="Horizontal">
                                    <RadioButton
                                        Content="{Binding Text}"
                                        IsChecked="{Binding IsSelected}"
                                        GroupName="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType=local:QuestionAndAnswers}, Path=Question}"
                                        Margin="0,0,10,0"
                                    />
                                </StackPanel>
                            </DataTemplate>
                        </ItemsControl.ItemTemplate>
                    </ItemsControl>
                </StackPanel>
            </DataTemplate>
        </ItemsControl.ItemTemplate>
    </ItemsControl>
</UserControl>

以及以下QuestionnaireControl.xaml.cs

public partial class QuestionnaireControl : UserControl
{
    public QuestionnaireControl()
    {
        InitializeComponent();

        if (System.ComponentModel.DesignerProperties.GetIsInDesignMode(this))
        {
            Questions = new List<QuestionAndAnswers> {
                new QuestionAndAnswers() { Number=1, Question="Do you like pizza?" },
                new QuestionAndAnswers() { Number=2, Question="Can you surf?" },
                new QuestionAndAnswers() { Number=3, Question="Are you funny?" },
                new QuestionAndAnswers() { Number=4, Question="Is Monday your favorite day of the week?" },
                new QuestionAndAnswers() { Number=5, Question="Have you been to Paris?" },
                new QuestionAndAnswers() { Number=6, Question="When sleeping, do you snore?" },
                new QuestionAndAnswers() { Number=7, Question="Could you be living in a dream?" }
            };
            Answers = new List<Answer> {
                new Answer() { Value=1, Text="Yes", IsSelected=false },
                new Answer() { Value=2, Text="No", IsSelected=false },
                new Answer() { Value=3, Text="Sort Of", IsSelected=false },
            };
        }
        else
        {
            Questions = new List<QuestionAndAnswers>();
            Answers = new List<Answer>();
        }

        // Copy Answers to each QuestionAndAnswers.
        foreach (QuestionAndAnswers qa in Questions)
        {
            qa.Answers = new List<Answer>(Answers);
        }
    }

    public List<QuestionAndAnswers> Questions
    {
        get { return (List<QuestionAndAnswers>)GetValue(QuestionsProperty); }
        set { SetValue(QuestionsProperty, value); }
    }

    public static readonly DependencyProperty QuestionsProperty =
        DependencyProperty.Register("Questions", typeof(List<QuestionAndAnswers>), typeof(QuestionnaireControl), new FrameworkPropertyMetadata(new List<QuestionAndAnswers>()));

    public List<Answer> Answers
    {
        get { return (List<Answer>)GetValue(AnswersProperty); }
        set { SetValue(AnswersProperty, value); }
    }

    public static readonly DependencyProperty AnswersProperty =
        DependencyProperty.Register("Answers", typeof(List<Answer>), typeof(QuestionnaireControl), new FrameworkPropertyMetadata(new List<Answer>()));
}

public class QuestionAndAnswers
{
    public int Number { get; set; }
    public string Question { get; set; }
    public List<Answer> Answers { get; set; }
}

public class Answer
{
    public string Text { get; set; }
    public int Value { get; set; }
    public bool IsSelected { get; set; }
}

使用上面的代码,我可以在 Visual Studio 设计器中生成上面 QuestionnaireControl 的图像。但是当我实际使用 QuestionnaireControl 时,根据上面的例子,问题被呈现而不是答案。有谁知道我需要调整什么?

为每个问题复制答案选项的代码无法正常工作。 For once 它只在构造函数中运行一次,而不是在添加答案之后运行。此外,它不会创建 Answer 对象的新实例,因此所有问题都保持相同的引用,并且当为一个问题选择第一个选项时,它会立即为所有其他问题选择。每个问题都需要自己的一组答案:

public class QuestionAndAnswers
{
    public QuestionAndAnswers()
    {
       Answers = new List<Answer>();
    }
    public int Number { get; set; }
    public string Question { get; set; }
    public List<Answer> Answers { get; set; }
}
<local:QuestionnaireControl>
  <local:QuestionnaireControl.Questions>

    <local:QuestionAndAnswers Number="1" Question="Is this working?">
      <local:QuestionAndAnswers.Answers>
       <local:Answer Value="0" Text="Yes" />
       <local:Answer Value="1" Text="No" IsSelected="true"/>
       <local:Answer Value="2" Text="Help Me Please" />
      </local:QuestionAndAnswers.Answers>
    </local:QuestionAndAnswers>

    <local:QuestionAndAnswers Number="2" Question="Are these questions sharing answers?">
      <local:QuestionAndAnswers.Answers>
       <local:Answer Value="0" Text="Yes" IsSelected="true"/>
       <local:Answer Value="1" Text="No" />
       <local:Answer Value="2" Text="Help Me Please" />
      </local:QuestionAndAnswers.Answers>
    </local:QuestionAndAnswers>

  </local:QuestionnaireControl.Questions>
</local:QuestionnaireControl>

如果您将控件的 Answers 属性 类型更改为 ObservableCollection<Answer> 您可以处理它的 CollectionChanged 事件并复制每个问题的答案:

public partial class QuestionnaireControl : UserControl
{
    public QuestionnaireControl()
    {
        InitializeComponent();
        Questions = new List<QuestionAndAnswers>();
        Answers = new ObservableCollection<Answer>();
        Answers.CollectionChanged += Answers_CollectionChanged;
    }

    private void Answers_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
    {
        foreach (var question in Questions)
        {
            question.Answers = new List<Answer>();
            foreach (var answer in Answers)
            {
                question.Answers.Add(new Answer() { Text = answer.Text, Value = answer.Value, IsSelected = answer.IsSelected });
            }
        }
    }

    public List<QuestionAndAnswers> Questions
    {
        get { return (List<QuestionAndAnswers>)GetValue(QuestionsProperty); }
        set { SetValue(QuestionsProperty, value); }
    }

    public static readonly DependencyProperty QuestionsProperty =
        DependencyProperty.Register("Questions", typeof(List<QuestionAndAnswers>), typeof(QuestionnaireControl));

    public ObservableCollection<Answer> Answers
    {
        get { return (ObservableCollection<Answer>)GetValue(AnswersProperty); }
        set { SetValue(AnswersProperty, value); }
    }

    public static readonly DependencyProperty AnswersProperty =
        DependencyProperty.Register("Answers", typeof(ObservableCollection<Answer>), typeof(QuestionnaireControl), new FrameworkPropertyMetadata(null));

}

复制 Answer 对象 (new Answer() {...}) 很容易,但是检测它应该发生的时刻是很棘手的。 AnswersProperty 仅更改 1 次(当分配新的 List<Answer> 时),然后项目被添加到该列表中,我们无法获得通知。而且我们不能在 xaml 中创建通用列表(标记限制)。然而,一个已知的解决方法是创建从通用集合派生的专门集合。这是一个完整的示例(您可能希望将 INotifyPropertyChnaged 实现添加到 AnswerQuestionAndAnswers 类):

public class QuestionAndAnswers
{
    public QuestionAndAnswers()
    {
        Answers = new ObservableCollection<Answer>();
    }
    public int Number { get; set; }
    public string Question { get; set; }
    public ObservableCollection<Answer> Answers { get; private set; }
}

public class Answer : ICloneable
{
    public string Text { get; set; }
    public int Value { get; set; }
    public bool IsSelected { get; set; }

    public object Clone()
    {
        return MemberwiseClone();
    }
}

public class QuestionCollection : List<QuestionAndAnswers>
{
}

public class AnswerCollection : List<Answer>
{
}
public partial class QuestionnaireControl : UserControl
{
    public QuestionnaireControl()
    {
        InitializeComponent();
    }

    public List<QuestionAndAnswers> Questions
    {
        get { return (List<QuestionAndAnswers>) GetValue(QuestionsProperty); }
        set { SetValue(QuestionsProperty, value); }
    }

    public static readonly DependencyProperty QuestionsProperty =
        DependencyProperty.Register("Questions", typeof (List<QuestionAndAnswers>), typeof (QuestionnaireControl),
            new PropertyMetadata(new List<QuestionAndAnswers>(), QuestionsChangedCallback));

    private static void QuestionsChangedCallback(DependencyObject o, DependencyPropertyChangedEventArgs e)
    {
        var q = o as QuestionnaireControl;
        if (q == null)
            return;

        CopyAnswers(q);
    }

    public List<Answer> Answers
    {
        get { return (List<Answer>) GetValue(AnswersProperty); }
        set { SetValue(AnswersProperty, value); }
    }

    public static readonly DependencyProperty AnswersProperty =
            DependencyProperty.Register("Answers", typeof(List<Answer>), typeof(QuestionnaireControl),
                new PropertyMetadata(new List<Answer>(), AnswersChangedCallback));

    private static void AnswersChangedCallback(DependencyObject o, DependencyPropertyChangedEventArgs e)
    {
        var q = o as QuestionnaireControl;
        if (q == null)
            return;

        CopyAnswers(q);
    }

    private static void CopyAnswers(QuestionnaireControl q)
    {
        if (q.Answers == null || q.Questions == null)
            return;

        foreach (var question in q.Questions)
        {
            // remove old Answers
            question.Answers.Clear();
            // adding new Answers to each question
            foreach (var answer in q.Answers)
                question.Answers.Add((Answer) answer.Clone());
        }
    }
}
<UserControl x:Class="WpfDemos.QuestionnaireControl"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             mc:Ignorable="d"
             DataContext="{Binding RelativeSource={RelativeSource Self}}">    
    <ItemsControl ItemsSource="{Binding Questions}">
        <ItemsControl.ItemsPanel>
            <ItemsPanelTemplate>
                <StackPanel Orientation="Vertical"/>
            </ItemsPanelTemplate>
        </ItemsControl.ItemsPanel>
        <ItemsControl.ItemTemplate>
            <DataTemplate>
                <StackPanel Orientation="Horizontal">
                    <TextBlock Text="{Binding Number, StringFormat='{}{0}.'}" Margin="0,0,10,0" />
                    <TextBlock Text="{Binding Question}" Width="220"/>
                    <ItemsControl ItemsSource="{Binding Answers}">
                        <ItemsControl.ItemsPanel>
                            <ItemsPanelTemplate>
                                <StackPanel Orientation="Horizontal"/>
                            </ItemsPanelTemplate>
                        </ItemsControl.ItemsPanel>
                        <ItemsControl.ItemTemplate>
                            <DataTemplate>
                                <RadioButton
                                        Content="{Binding Path=Text}"
                                        IsChecked="{Binding Path=IsSelected}"
                                        GroupName="{Binding Path=DataContext.Question, RelativeSource={RelativeSource AncestorType=ItemsControl}}"
                                        Margin="0,0,10,0"/>
                            </DataTemplate>
                        </ItemsControl.ItemTemplate>
                    </ItemsControl>
                </StackPanel>
            </DataTemplate>
        </ItemsControl.ItemTemplate>
    </ItemsControl>
</UserControl>
<Window x:Class="WpfDemos.MyWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:wpfDemos="clr-namespace:WpfDemos"        
        Title="Questionnaire" 
        Height="300" Width="480">
    <wpfDemos:QuestionnaireControl>

        <wpfDemos:QuestionnaireControl.Questions>
            <wpfDemos:QuestionCollection>
                <wpfDemos:QuestionAndAnswers Number="1" Question="Is this working?" />
                <wpfDemos:QuestionAndAnswers Number="2" Question="Are these questions sharing answers?" />
            </wpfDemos:QuestionCollection>
        </wpfDemos:QuestionnaireControl.Questions>

        <wpfDemos:QuestionnaireControl.Answers>
            <wpfDemos:AnswerCollection>
                <wpfDemos:Answer Value="0" Text="Yes" />
                <wpfDemos:Answer Value="1" Text="No" />
                <wpfDemos:Answer Value="2" Text="Help Me Please" />
            </wpfDemos:AnswerCollection>
        </wpfDemos:QuestionnaireControl.Answers>

    </wpfDemos:QuestionnaireControl>
</Window>

工作原理:AnswersPropertyQuestionsProperty 属性 更改了我们复制问题答案的回调。 And 触发回调是因为我们创建了新集合 (<wpfDemos:QuestionCollection>,<wpfDemos:AnswerCollection>) :)