【问题标题】:How can I build a questionnaire UserControl in XAML whose questions share the same answers?如何在 XAML 中构建问题共享相同答案的问卷 UserControl?
【发布时间】:2017-06-16 19:14:49
【问题描述】:

我想建立一个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 时,根据上面的示例,会呈现问题但不会呈现答案。有谁知道我需要调整什么?

【问题讨论】:

  • // Copy Answers to each QuestionAndAnswers. 部分只工作一次。在设计模式下,它有一些数据要复制。在启动应用程序后添加来自&lt;local:QuestionnaireControl.Answers&gt; 的答案后,它不会运行。
  • @ASh 在添加来自 XAML 的答案后,如何复制答案以运行?我尝试将该代码放入 Loaded 回调中,但没有奏效。

标签: c# wpf xaml user-controls


【解决方案1】:

复制Answer 对象(new Answer() {...}) 很容易,但是检测应该发生的时刻却很棘手。 AnswersProperty 仅更改 1 次(分配新的 List&lt;Answer&gt; 时),然后将项目添加到该列表中,我们无法获得通知。而且我们不能在 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 有属性更改回调,我们复制问题的答案。 并且因为我们创建了新集合 (&lt;wpfDemos:QuestionCollection&gt;,&lt;wpfDemos:AnswerCollection&gt;) :)

【讨论】:

  • 我很欣赏额外的代码和解释,但mm8 早先给出了基本答案,所以我选择了他作为答案。 +1 用于注意和修复 GroupName 绑定!
  • @MichaelRepucci, Answers.CollectionChanged += Answers_CollectionChanged; 在构造函数中不可靠:如果用户将新集合分配给公共 Answers 属性,问题将不会更新,Answers_CollectionChanged 将不再触发跨度>
【解决方案2】:

如果您将控件的Answers 属性类型更改为ObservableCollection&lt;Answer&gt;,您可以处理其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));

}

【讨论】:

    【解决方案3】:

    复制每个问题的答案选项的代码工作不正确。一次它只在构造函数中运行一次,而不是在添加答案之后。此外,它不会创建 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>
    

    【讨论】:

    • 我明确希望避免 XAML 中每个问题的答案重复,因为这很容易导致错误。
    • @MichaelRepucci,请参阅另一个答案。请注意,我没有在 .ctor 中设置设计时数据,因为设计师应该显示来自问答列表的真实数据。还要注意控制 xaml 的变化(特别是GroupName 绑定)
    • @MichaelRepucci,您能否就我发布的替代解决方案提供任何反馈?
    • 对不起,有一百万件事情要做,而这只是一件。我应该能够在本周末之前回到这个问题上。感谢您提出的解决方案!
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2014-06-06
    • 1970-01-01
    • 2012-04-11
    • 1970-01-01
    • 2020-10-30
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多