使用richtextbox的运行作为itemscontrol

时间:2017-09-05 20:03:09

标签: c# wpf xaml mvvm

要显示“动态”数据,一种简单的方法就是使用ItemsControl(例如,WrapPanel作为项目模板。)

现在我希望我的应用程序,一个充满运行的富文本框是理想的。 - 运行的(数字和数据)取决于我的viewmodel中的可观察集合。如果我使用WrapPanel而不是RichTextBox,则itemscontrol代码将如下所示:

<ItemsControl ItemsSource="{Binding Data}">
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <WrapPanel IsItemsHost="True">
            </WrapPanel>
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
</ItemsControl>

现在我尝试在usercontrol中使用richtextbox,用户控件的xaml如下所示:

<UserControl x:Class="testit.MyControl"
             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" 
             xmlns:local="clr-namespace:testit"
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300">
    <UserControl.Resources>
        <DataTemplate DataType="{x:Type local:DispData}">
            <TextBlock>
                <Run Text="{Binding Text}"></Run>
            </TextBlock>
        </DataTemplate>
    </UserControl.Resources>
    <StackPanel>
        <RichTextBox IsReadOnly="True" IsDocumentEnabled="True" VerticalScrollBarVisibility="Auto">
            <FlowDocument>
                <ItemsControl ItemsSource="{Binding Data}">
                    <ItemsControl.ItemsPanel>
                        <ItemsPanelTemplate>
                            <Paragraph IsItemsHost="True">
                            </Paragraph>
                        </ItemsPanelTemplate>
                    </ItemsControl.ItemsPanel>
                </ItemsControl>
            </FlowDocument>
        </RichTextBox>
        <ItemsControl ItemsSource="{Binding Data}">
            <ItemsControl.ItemsPanel>
                <ItemsPanelTemplate>
                    <WrapPanel IsItemsHost="True">
                    </WrapPanel>
                </ItemsPanelTemplate>
            </ItemsControl.ItemsPanel>
        </ItemsControl>
    </StackPanel>
</UserControl>

绑定到usercontrol的datacontext的viewmodel是:

namespace testit
{
    class ViewModel : INotifyPropertyChanged
    {

        private readonly ObservableCollection<DispData> _data =
            new ObservableCollection<DispData>();
        public ReadOnlyObservableCollection<DispData> Data { get; private set; }
        public ViewModel() {
            Data = new ReadOnlyObservableCollection<DispData>(_data);
            _data.Add(new DispData("hello"));
            _data.Add(new DispData("world"));
        }
        public event PropertyChangedEventHandler PropertyChanged;

        [NotifyPropertyChangedInvocator]
        protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

这会给RichTextBox带来很多错误(第一个错误是ItemsControl无法放在那里,而另一个错误是Paragraph没有IsItemsHost我要强调的是,如果我注释掉richtextbox xaml,包装面板的xaml确实有用:所以它不是绑定或任何错误。

RichTextBox甚至可以与ItemsControl一起使用 - 如果没有,我将如何以MVVM方式填充文本框的内容?

1 个答案:

答案 0 :(得分:3)

您应该查看this article如何编写与FlowDocumentRichTextBox兼容的自己的项目控件。可以在此location

找到代码示例

下载控件后,请更新GenerateContent()ItemsContent方法中的if-else条件,如下所示,以添加对ParagraphInlines的支持。

private void GenerateContent(DataTemplate itemsPanel, DataTemplate itemTemplate, IEnumerable itemsSource)
{

....

    if (panel is Section)
        ((Section)panel).Blocks.Add(Helpers.ConvertToBlock(data, element));
    else if (panel is TableRowGroup)
        ((TableRowGroup)panel).Rows.Add((TableRow)element);
    else if (panel is Paragraph && element is Inline)
        ((Paragraph)panel).Inlines.Add((Inline)element);
    else
        throw new Exception(String.Format("Don't know how to add an instance of {0} to an instance of {1}", element.GetType(), panel.GetType()));

并将您的XAML更新为:

<RichTextBox IsReadOnly="True" IsDocumentEnabled="True" VerticalScrollBarVisibility="Auto">
    <FlowDocument>
        <flowdoc:ItemsContent ItemsSource ItemsSource="{Binding Data}">                
            <flowdoc:ItemsContent.ItemsPanel>
                <DataTemplate>
                    <flowdoc:Fragment>
                        <Paragraph flowdoc:Attached.IsItemsHost="True" />
                    </flowdoc:Fragment>
                </DataTemplate>
            </flowdoc:ItemsContent.ItemsPanel>
            <flowdoc:ItemsContent.ItemTemplate>
                <DataTemplate>
                    <flowdoc:Fragment>
                        <flowdoc:BindableRun BoundText="{Binding Text}" />
                    </flowdoc:Fragment>
                </DataTemplate>
            </flowdoc:ItemsContent.ItemTemplate>
        </flowdoc:ItemsContent>
    </FlowDocument>
</RichTextBox>

编辑 - 1 正如@ ed-plunkett建议的那样,在这里共享相关代码(如果外部链接不起作用)

  • 为了能够在item-template中使用Run(类似于标签或文本块);您需要扩展Run以添加可绑定属性。

    public class BindableRun : Run
    {
        public static readonly DependencyProperty BoundTextProperty = DependencyProperty.Register("BoundText", typeof(string), typeof(BindableRun), new PropertyMetadata(OnBoundTextChanged));
    
        public BindableRun()
        {
            Helpers.FixupDataContext(this);
        }
    
        private static void OnBoundTextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            ((Run)d).Text = (string)e.NewValue;
        }
    
        public String BoundText
        {
            get { return (string)GetValue(BoundTextProperty); }
            set { SetValue(BoundTextProperty, value); }
        }
    }
    
  • 接下来,您需要能够将容器控件标记为项目主机;这可以通过定义附加属性来完成。

    public class Attached
    {
        private static readonly DependencyProperty IsItemsHostProperty = DependencyProperty.RegisterAttached("IsItemsHost", typeof(bool), typeof(Attached), new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.NotDataBindable, OnIsItemsHostChanged));
        private static readonly DependencyProperty ItemsHostProperty = DependencyProperty.RegisterAttached("ItemsHost", typeof(FrameworkContentElement), typeof(Attached), new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.NotDataBindable));
    
        public static bool GetIsItemsHost(DependencyObject target)
        {
            return (bool)target.GetValue(IsItemsHostProperty);
        }
    
    
        public static void SetIsItemsHost(DependencyObject target, bool value)
        {
            target.SetValue(IsItemsHostProperty, value);
        }
    
    
        private static void SetItemsHost(FrameworkContentElement element)
        {
            FrameworkContentElement parent = element;
            while (parent.Parent != null)
                parent = (FrameworkContentElement)parent.Parent;
            parent.SetValue(ItemsHostProperty, element);
        }
    
    
        public static FrameworkContentElement GetItemsHost(DependencyObject dp)
        {
            return (FrameworkContentElement)dp.GetValue(ItemsHostProperty);
        }
    
        private static void OnIsItemsHostChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            if ((bool)e.NewValue)
            {
                FrameworkContentElement element = (FrameworkContentElement)d;
                if (element.IsInitialized)
                    SetItemsHost(element);
                else
                    element.Initialized += ItemsHost_Initialized;
            }
        }
    
        private static void ItemsHost_Initialized(object sender, EventArgs e)
        {
            FrameworkContentElement element = (FrameworkContentElement)sender;
            element.Initialized -= ItemsHost_Initialized;
            SetItemsHost(element);
        }
    } 
    
  • 可用于在FrameworkContentElement内嵌入DataTemplate的片段控件。

    [ContentProperty("Content")]
    public class Fragment : FrameworkElement
    {
        private static readonly DependencyProperty ContentProperty = DependencyProperty.Register("Content", typeof(FrameworkContentElement), typeof(Fragment));
    
        public FrameworkContentElement Content
        {
            get
            {
                return (FrameworkContentElement)GetValue(ContentProperty);
            }
            set
            {
                SetValue(ContentProperty, value);
            }
        }
    }
    
  • 而且,最后这些项目控制着自己:这是重大举措:

    public class ItemsContent : Section
    {
        private static readonly DependencyProperty ItemsSourceProperty = DependencyProperty.Register("ItemsSource", typeof(IEnumerable), typeof(ItemsContent), new PropertyMetadata(OnItemsSourceChanged));
        private static readonly DependencyProperty ItemTemplateProperty = DependencyProperty.Register("ItemTemplate", typeof(DataTemplate), typeof(ItemsContent), new PropertyMetadata(OnItemTemplateChanged));
        private static readonly DependencyProperty ItemsPanelProperty = DependencyProperty.Register("ItemsPanel", typeof(DataTemplate), typeof(ItemsContent), new PropertyMetadata(OnItemsPanelChanged));
    
        public ItemsContent()
        {
            Helpers.FixupDataContext(this);
            Loaded += ItemsContent_Loaded;
        }
    
        private void ItemsContent_Loaded(object sender, RoutedEventArgs e)
        {
            GenerateContent(ItemsPanel, ItemTemplate, ItemsSource);
        }
    
        public IEnumerable ItemsSource
        {
            get { return (IEnumerable)GetValue(ItemsSourceProperty); }
            set { SetValue(ItemsSourceProperty, value); }
        }
    
        public DataTemplate ItemTemplate
        {
            get { return (DataTemplate)GetValue(ItemTemplateProperty); }
            set { SetValue(ItemTemplateProperty, value); }
        }
    
        public DataTemplate ItemsPanel
        {
            get { return (DataTemplate)GetValue(ItemsPanelProperty); }
            set { SetValue(ItemsPanelProperty, value); }
        }
    
        private void GenerateContent(DataTemplate itemsPanel, DataTemplate itemTemplate, IEnumerable itemsSource)
        {
            Blocks.Clear();
            if (itemTemplate != null && itemsSource != null)
            {
                FrameworkContentElement panel = null;
    
                foreach (object data in itemsSource)
                {
                    if (panel == null)
                    {
                        if (itemsPanel == null)
                            panel = this;
                        else
                        {
                            FrameworkContentElement p = Helpers.LoadDataTemplate(itemsPanel);
                            if (!(p is Block))
                                throw new Exception("ItemsPanel must be a block element");
                            Blocks.Add((Block)p);
                            panel = Attached.GetItemsHost(p);
                            if (panel == null)
                                throw new Exception("ItemsHost not found. Did you forget to specify Attached.IsItemsHost?");                            
                        }
                    }
                    FrameworkContentElement element = Helpers.LoadDataTemplate(itemTemplate);
                    element.DataContext = data;
                    Helpers.UnFixupDataContext(element);
                    if (panel is Section)
                        ((Section)panel).Blocks.Add(Helpers.ConvertToBlock(data, element));
                    else if (panel is TableRowGroup)
                        ((TableRowGroup)panel).Rows.Add((TableRow)element);
                    else if (panel is Paragraph && element is Inline)
                        ((Paragraph)panel).Inlines.Add((Inline)element);
                    else
                        throw new Exception(String.Format("Don't know how to add an instance of {0} to an instance of {1}", element.GetType(), panel.GetType()));
                }
            }
        }
    
        private void GenerateContent()
        {
            GenerateContent(ItemsPanel, ItemTemplate, ItemsSource);
        }
    
        private void OnItemsSourceChanged(IEnumerable newValue)
        {
            if (IsLoaded)
                GenerateContent(ItemsPanel, ItemTemplate, newValue);
        }
    
        private void OnItemTemplateChanged(DataTemplate newValue)
        {
            if (IsLoaded)
                GenerateContent(ItemsPanel, newValue, ItemsSource);
        }
    
        private void OnItemsPanelChanged(DataTemplate newValue)
        {
            if (IsLoaded)
                GenerateContent(newValue, ItemTemplate, ItemsSource);
        }
    
        private static void OnItemsSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            ((ItemsContent)d).OnItemsSourceChanged((IEnumerable)e.NewValue);
        }
    
        private static void OnItemTemplateChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            ((ItemsContent)d).OnItemTemplateChanged((DataTemplate)e.NewValue);
        }
    
        private static void OnItemsPanelChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            ((ItemsContent)d).OnItemsPanelChanged((DataTemplate)e.NewValue);
        }
    }
    
  • 静态辅助方法:

    internal static class Helpers
    {
        /// <summary>
        /// If you use a bindable flow document element more than once, you may encounter a "Collection was modified" exception.
        /// The error occurs when the binding is updated because of a change to an inherited dependency property. The most common scenario 
        /// is when the inherited DataContext changes. It appears that an inherited properly like DataContext is propagated to its descendants. 
        /// When the enumeration of descendants gets to a BindableXXX, the dependency properties of that element change according to the new 
        /// DataContext, which change the (non-dependency) properties. However, for some reason, changing the flow content invalidates the 
        /// enumeration and raises an exception. 
        /// To work around this, one can either DataContext="{Binding DataContext, RelativeSource={RelativeSource AncestorType=FrameworkElement}}" 
        /// in code. This is clumsy, so every derived type calls this function instead (which performs the same thing).
        /// See http://code.logos.com/blog/2008/01/data_binding_in_a_flowdocument.html
        /// </summary>
        /// <param name="element"></param>
        public static void FixupDataContext(FrameworkContentElement element)
        {
            Binding b = new Binding(FrameworkContentElement.DataContextProperty.Name);
            // another approach (if this one has problems) is to bind to an ancestor by ElementName
            b.RelativeSource = new RelativeSource(RelativeSourceMode.FindAncestor, typeof(FrameworkElement), 1);
            element.SetBinding(FrameworkContentElement.DataContextProperty, b);
        }
    
    
        private static bool InternalUnFixupDataContext(DependencyObject dp)
        {
            // only consider those elements for which we've called FixupDataContext(): they all belong to this namespace
            if (dp is FrameworkContentElement && dp.GetType().Namespace == typeof(Helpers).Namespace)
            {
                Binding binding = BindingOperations.GetBinding(dp, FrameworkContentElement.DataContextProperty);
                if (binding != null
                    && binding.Path != null && binding.Path.Path == FrameworkContentElement.DataContextProperty.Name
                    && binding.RelativeSource != null && binding.RelativeSource.Mode == RelativeSourceMode.FindAncestor && binding.RelativeSource.AncestorType == typeof(FrameworkElement) && binding.RelativeSource.AncestorLevel == 1)
                {
                    BindingOperations.ClearBinding(dp, FrameworkContentElement.DataContextProperty);
                    return true;
                }
            }
            // as soon as we have disconnected a binding, return. Don't continue the enumeration, since the collection may have changed
            foreach (object child in LogicalTreeHelper.GetChildren(dp))
                if (child is DependencyObject)
                    if (InternalUnFixupDataContext((DependencyObject)child))
                        return true;
            return false;
        }
    
    
        public static void UnFixupDataContext(DependencyObject dp)
        {
            while (InternalUnFixupDataContext(dp))
                ;
        }
    
    
        /// <summary>
        /// Convert "data" to a flow document block object. If data is already a block, the return value is data recast.
        /// </summary>
        /// <param name="dataContext">only used when bindable content needs to be created</param>
        /// <param name="data"></param>
        /// <returns></returns>
        public static Block ConvertToBlock(object dataContext, object data)
        {
            if (data is Block)
                return (Block)data;
            else if (data is Inline)
                return new Paragraph((Inline)data);
            else if (data is BindingBase)
            {
                BindableRun run = new BindableRun();
                if (dataContext is BindingBase)
                    run.SetBinding(BindableRun.DataContextProperty, (BindingBase)dataContext);
                else
                    run.DataContext = dataContext;
                run.SetBinding(BindableRun.BoundTextProperty, (BindingBase)data);
                return new Paragraph(run);
            }
            else
            {
                Run run = new Run();
                run.Text = (data == null) ? String.Empty : data.ToString();
                return new Paragraph(run);
            }
        }
    
    
        public static FrameworkContentElement LoadDataTemplate(DataTemplate dataTemplate)
        {
            object content = dataTemplate.LoadContent();
            if (content is Fragment)
                return (FrameworkContentElement)((Fragment)content).Content;
            else if (content is TextBlock)
            {
                InlineCollection inlines = ((TextBlock)content).Inlines;
                if (inlines.Count == 1)
                    return inlines.FirstInline;
                else
                {
                    Paragraph paragraph = new Paragraph();
                    // we can't use an enumerator, since adding an inline removes it from its collection
                    while (inlines.FirstInline != null)
                        paragraph.Inlines.Add(inlines.FirstInline);
                    return paragraph;
                }
            }
            else
                throw new Exception("Data template needs to contain a <Fragment> or <TextBlock>");
        }
    }