数据绑定 TextBlock.Inlines

发布于 2024-08-15 13:20:31 字数 246 浏览 5 评论 0原文

我的 WPF 应用程序从后端服务接收消息流,我需要将其显示在 UI 中。这些消息差异很大,我希望每条消息都有不同的视觉布局(字符串格式、颜色、字体、图标等)。

我希望能够为每条消息创建一个内联(Run、TextBlock、Italic 等),然后以某种方式将它们全部放入 ObservableCollection<> 中,并在我的设备上使用 WPF 数据绑定的魔力。 UI 中的 TextBlock.Inlines。我找不到如何做到这一点,这可能吗?

My WPF App receives a stream of messages from a backend service that I need to display in the UI. These messages vary widely and I want to have different visual layout (string formats, colors, Fonts, icons, whatever etc.) for each message.

I was hoping to just be able to create an inline (Run, TextBlock, Italic etc) for each message then somehow put them all in a ObservableCollection<> and using he magic of WPF Data Binding on my TextBlock.Inlines in the UI. I couldn't find how to do this, is this possible?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(10

星光不落少年眉 2024-08-22 13:20:31

您可以将依赖属性添加到 TextBlock 子类

public class BindableTextBlock : TextBlock
{
    public ObservableCollection<Inline> InlineList
    {
        get { return (ObservableCollection<Inline>)GetValue(InlineListProperty); }
        set { SetValue(InlineListProperty, value); }
    }

    public static readonly DependencyProperty InlineListProperty =
        DependencyProperty.Register("InlineList",typeof(ObservableCollection<Inline>), typeof(BindableTextBlock), new UIPropertyMetadata(null, OnPropertyChanged));

    private static void OnPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
    {
        BindableTextBlock textBlock = sender as BindableTextBlock;
        ObservableCollection<Inline> list = e.NewValue as ObservableCollection<Inline>;
        list.CollectionChanged += new     System.Collections.Specialized.NotifyCollectionChangedEventHandler(textBlock.InlineCollectionChanged);
    }

    private void InlineCollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
    {
        if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Add)
        {
            int idx = e.NewItems.Count -1;
            Inline inline = e.NewItems[idx] as Inline;
            this.Inlines.Add(inline);
        }
    }
}

You could add a Dependency Property to a TextBlock Subclass

public class BindableTextBlock : TextBlock
{
    public ObservableCollection<Inline> InlineList
    {
        get { return (ObservableCollection<Inline>)GetValue(InlineListProperty); }
        set { SetValue(InlineListProperty, value); }
    }

    public static readonly DependencyProperty InlineListProperty =
        DependencyProperty.Register("InlineList",typeof(ObservableCollection<Inline>), typeof(BindableTextBlock), new UIPropertyMetadata(null, OnPropertyChanged));

    private static void OnPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
    {
        BindableTextBlock textBlock = sender as BindableTextBlock;
        ObservableCollection<Inline> list = e.NewValue as ObservableCollection<Inline>;
        list.CollectionChanged += new     System.Collections.Specialized.NotifyCollectionChangedEventHandler(textBlock.InlineCollectionChanged);
    }

    private void InlineCollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
    {
        if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Add)
        {
            int idx = e.NewItems.Count -1;
            Inline inline = e.NewItems[idx] as Inline;
            this.Inlines.Add(inline);
        }
    }
}
记忆消瘦 2024-08-22 13:20:31

这是不可能的,因为 TextBlock.Inlines 属性不是依赖属性。只有依赖属性才能成为数据绑定的目标。

根据您的具体布局要求,您可以使用 ItemsControl 来完成此操作,将其 ItemsPanel 设置为 WrapPanel 并将其 ItemsSource 设置为您的集合。 (这里可能需要进行一些实验,因为 Inline 不是 UIElement,因此它的默认渲染可能会使用 ToString() 而不是)

或者,您可能需要构建一个新控件,例如 MultipartTextBlock,并使用可绑定的 PartsSource 属性和 TextBlock 作为其默认值模板。设置 PartsSource 后,您的控件将附加一个 CollectionChanged 事件处理程序(直接或通过 CollectionChangedEventManager),并从代码更新 TextBlock.Inlines 集合随着 PartsSource 集合的更改。

无论哪种情况,如果您的代码直接生成 Inline 元素(因为 Inline 不能同时在两个地方使用),则可能需要小心。或者,您可能需要考虑公开文本、字体等的抽象模型(即视图模型)并通过 DataTemplate 创建实际的 Inline 对象。这也可以提高可测试性,但显然会增加复杂性和工作量。

This is not possible because the TextBlock.Inlines property is not a dependency property. Only dependency properties can be the target of a data binding.

Depending on your exact layout requirements you may be able to do this using an ItemsControl, with its ItemsPanel set to a WrapPanel and its ItemsSource set to your collection. (Some experimentation may be required here because an Inline is not a UIElement, so its default rendering will probably be done using ToString() rather than being displayed.)

Alternatively, you may need to build a new control, e.g. MultipartTextBlock, with a bindable PartsSource property and a TextBlock as its default template. When the PartsSource was set your control would attach a CollectionChanged event handler (directly or via CollectionChangedEventManager), and update the TextBlock.Inlines collection from code as the PartsSource collection changed.

In either case, caution may be required if your code is generating Inline elements directly (because an Inline can't be used in two places at the same time). You may alternatively want to consider exposing an abstract model of text, font, etc. (i.e. a view model) and creating the actual Inline objects via a DataTemplate. This may also improve testability, but obviously adds complexity and effort.

单挑你×的.吻 2024-08-22 13:20:31

这是利用 WPF 行为/附加属性的替代解决方案:

public static class TextBlockExtensions
{
    public static IEnumerable<Inline> GetBindableInlines ( DependencyObject obj )
    {
        return (IEnumerable<Inline>) obj.GetValue ( BindableInlinesProperty );
    }

    public static void SetBindableInlines ( DependencyObject obj, IEnumerable<Inline> value )
    {
        obj.SetValue ( BindableInlinesProperty, value );
    }

    public static readonly DependencyProperty BindableInlinesProperty =
        DependencyProperty.RegisterAttached ( "BindableInlines", typeof ( IEnumerable<Inline> ), typeof ( TextBlockExtensions ), new PropertyMetadata ( null, OnBindableInlinesChanged ) );

    private static void OnBindableInlinesChanged ( DependencyObject d, DependencyPropertyChangedEventArgs e )
    {
        var Target = d as TextBlock;

        if ( Target != null )
        {
            Target.Inlines.Clear ();
            Target.Inlines.AddRange ( (System.Collections.IEnumerable) e.NewValue );
        }
    }
}

在 XAML 中,按如下方式使用它:

<TextBlock MyBehaviors:TextBlockExtensions.BindableInlines="{Binding Foo}" />

这使您不必从 TextBlock 继承。它也可以使用 ObservableCollection 而不是 IEnumerable 来工作,在这种情况下,您需要订阅集合更改。

This is an alternative solution which utilizes WPF behaviors/attached properties:

public static class TextBlockExtensions
{
    public static IEnumerable<Inline> GetBindableInlines ( DependencyObject obj )
    {
        return (IEnumerable<Inline>) obj.GetValue ( BindableInlinesProperty );
    }

    public static void SetBindableInlines ( DependencyObject obj, IEnumerable<Inline> value )
    {
        obj.SetValue ( BindableInlinesProperty, value );
    }

    public static readonly DependencyProperty BindableInlinesProperty =
        DependencyProperty.RegisterAttached ( "BindableInlines", typeof ( IEnumerable<Inline> ), typeof ( TextBlockExtensions ), new PropertyMetadata ( null, OnBindableInlinesChanged ) );

    private static void OnBindableInlinesChanged ( DependencyObject d, DependencyPropertyChangedEventArgs e )
    {
        var Target = d as TextBlock;

        if ( Target != null )
        {
            Target.Inlines.Clear ();
            Target.Inlines.AddRange ( (System.Collections.IEnumerable) e.NewValue );
        }
    }
}

In your XAML, use it like this:

<TextBlock MyBehaviors:TextBlockExtensions.BindableInlines="{Binding Foo}" />

This saves you from having to inherit from TextBlock. It could just as well work using an ObservableCollection instead of IEnumerable, in that case you'd need to subscribe to collection changes.

打小就很酷 2024-08-22 13:20:31

在 WPF 版本 4 中,您将能够绑定到 Run 对象,这可能会解决您的问题。

我过去通过重写 ItemsControl 并将文本显示为 ItemsControl 中的项目解决了这个问题。查看 WPF 博士针对此类内容所做的一些教程:http://www.drwpf.com

In version 4 of WPF you will be be able to bind to a Run object, which may solve your problem.

I have solved this problem in the past by overriding an ItemsControl and displaying the text as items in the ItemsControl. Look at some of the tutorials that Dr. WPF has done on this kind of stuff: http://www.drwpf.com

乖乖公主 2024-08-22 13:20:31

感谢弗兰克提供的解决方案。我必须做一些小的改变才能使它适合我。

public class BindableTextBlock : TextBlock
{
    public ObservableCollection<Inline> InlineList
    {
        get { return (ObservableCollection<Inline>) GetValue(InlineListProperty); }
        set { SetValue(InlineListProperty, value); }
    }

    public static readonly DependencyProperty InlineListProperty =
        DependencyProperty.Register("InlineList", typeof (ObservableCollection<Inline>), typeof (BindableTextBlock), new UIPropertyMetadata(null, OnPropertyChanged));

    private static void OnPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
    {
        BindableTextBlock textBlock = (BindableTextBlock) sender;
        textBlock.Inlines.Clear();
        textBlock.Inlines.AddRange((ObservableCollection<Inline>) e.NewValue);
    }
}

Thanks Frank for your solution. I had to make a couple of minor changes to make it work for me.

public class BindableTextBlock : TextBlock
{
    public ObservableCollection<Inline> InlineList
    {
        get { return (ObservableCollection<Inline>) GetValue(InlineListProperty); }
        set { SetValue(InlineListProperty, value); }
    }

    public static readonly DependencyProperty InlineListProperty =
        DependencyProperty.Register("InlineList", typeof (ObservableCollection<Inline>), typeof (BindableTextBlock), new UIPropertyMetadata(null, OnPropertyChanged));

    private static void OnPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
    {
        BindableTextBlock textBlock = (BindableTextBlock) sender;
        textBlock.Inlines.Clear();
        textBlock.Inlines.AddRange((ObservableCollection<Inline>) e.NewValue);
    }
}
拥醉 2024-08-22 13:20:31

如果我正确地得到您的要求,您可以手动检查即将到来的消息,并且对于每条消息,您可以向 TextBlock.Inlines 属性添加一个元素。它不需要任何数据绑定。
我已经通过以下方式完成了此操作:

public string MyBindingPath
{
    get { return (string)GetValue(MyBindingPathProperty); }
    set { SetValue(MyBindingPathProperty, value); }
}

// Using a DependencyProperty as the backing store for MyBindingPath.  This enables animation, styling, binding, etc...
public static readonly DependencyProperty MyBindingPathProperty =
        DependencyProperty.Register("MyBindingPath", typeof(string), typeof(Window2), new UIPropertyMetadata(null, OnPropertyChanged));

private static void OnPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
    (sender as Window2).textBlock.Inlines.Add(new Run(e.NewValue.ToString()));
}

If i am getting your requirement correctly, you can manually check for the coming messages and for each message you can add an element to TextBlock.Inlines property. It will not take any DataBinding.
I have done this with the following:

public string MyBindingPath
{
    get { return (string)GetValue(MyBindingPathProperty); }
    set { SetValue(MyBindingPathProperty, value); }
}

// Using a DependencyProperty as the backing store for MyBindingPath.  This enables animation, styling, binding, etc...
public static readonly DependencyProperty MyBindingPathProperty =
        DependencyProperty.Register("MyBindingPath", typeof(string), typeof(Window2), new UIPropertyMetadata(null, OnPropertyChanged));

private static void OnPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
    (sender as Window2).textBlock.Inlines.Add(new Run(e.NewValue.ToString()));
}
那支青花 2024-08-22 13:20:31

帕维尔·安希库斯基 (Pavel Anhikouski) 的建议非常有效。这里缺少 MVVM 中数据绑定的部分。使用视图模型中的 AddTrace 属性将内容添加到窗口中的 OutputBlock。
不需要窗口中的支持属性 MyBindingPath。

视图模型:

private string _addTrace;
public string AddTrace
{
  get => _addTrace;
  set
  {
    _addTrace = value;
    NotifyPropertyChanged();
  }
}

public void StartTrace()
{
  AddTrace = "1\n";
  AddTrace = "2\n";
  AddTrace = "3\n";
}

TraceWindow.xaml:

  <Grid>
    <ScrollViewer Name="Scroller" Margin="0" Background="#FF000128">
      <TextBlock Name="OutputBlock"  Foreground="White" FontFamily="Consolas" Padding="10"/>
    </ScrollViewer>
  </Grid>

TraceWindow.xaml.cs:

public TraceWindow(TraceWindowModel context)
{
  DataContext = context;
  InitializeComponent();

  //bind MyBindingPathProperty to AddTrace
  Binding binding = new Binding("AddTrace");
  binding.Source = context;
  this.SetBinding(MyBindingPathProperty, binding);
}

public static readonly DependencyProperty MyBindingPathProperty =
        DependencyProperty.Register("MyBindingPath", typeof(string), typeof(TraceWindow), new UIPropertyMetadata(null, OnPropertyChanged));



private static void OnPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
  (sender as TraceWindow).OutputBlock.Inlines.Add(new Run(e.NewValue.ToString()));
}

The Suggestion from Pavel Anhikouski works perfectly. Here the missing part with databinding in MVVM. Use the AddTrace property in the viewmodel to add content to the OutputBlock in the window.
The backing property MyBindingPath in the window is not needed.

ViewModel:

private string _addTrace;
public string AddTrace
{
  get => _addTrace;
  set
  {
    _addTrace = value;
    NotifyPropertyChanged();
  }
}

public void StartTrace()
{
  AddTrace = "1\n";
  AddTrace = "2\n";
  AddTrace = "3\n";
}

TraceWindow.xaml:

  <Grid>
    <ScrollViewer Name="Scroller" Margin="0" Background="#FF000128">
      <TextBlock Name="OutputBlock"  Foreground="White" FontFamily="Consolas" Padding="10"/>
    </ScrollViewer>
  </Grid>

TraceWindow.xaml.cs:

public TraceWindow(TraceWindowModel context)
{
  DataContext = context;
  InitializeComponent();

  //bind MyBindingPathProperty to AddTrace
  Binding binding = new Binding("AddTrace");
  binding.Source = context;
  this.SetBinding(MyBindingPathProperty, binding);
}

public static readonly DependencyProperty MyBindingPathProperty =
        DependencyProperty.Register("MyBindingPath", typeof(string), typeof(TraceWindow), new UIPropertyMetadata(null, OnPropertyChanged));



private static void OnPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
  (sender as TraceWindow).OutputBlock.Inlines.Add(new Run(e.NewValue.ToString()));
}
揽月 2024-08-22 13:20:31

最近我有一个类似的任务需要解决,即;可以将无限数量的 url 链接插入到自定义消息框文本内容中,并且具有此文本的绑定路径。
我决定在这里发布我的实现,因为这个线程有一些不同伟大想法的演变...这是我的解决方案:

概念
xaml TextBlock 内容的流程:

<TextBlock>
  ...
  <Inline>
  <Hyperlink <Inline>>
  <Inline>
  <Hyperlink <Inline>>
  ...
  1. 我的 x:Name=MixedText TextBlock 元素接收其值作为单个文本,格式为:

"...这里有一些文本...[链接-text|url-link]...此处还有一些其他文本...等。”

示例:

“请访问 Microsoft [site|https://www. microsoft.com/en-us/windows/windows-7-end-of-life-support-information],并下载 Windows 7 SP1,完成 SP1 安装,然后再次重新运行安装程序。 [roblox|https://www.roblox.com] 网站,像我儿子 \u263A 一样放松。”

  1. 我进行解析并将所有元素注入到我的 DataContextChanged 事件中的 MixedText TextBlock 元素。

xaml 部分:定义绑定路径 (MixedText)。
...

         <TextBlock Grid.Row="3" Grid.Column="1" 
                    x:Name="HyperlinkContent" 
                    TextWrapping="Wrap"
                    VerticalAlignment="Top"
                    HorizontalAlignment="Left"
                    Text="{Binding Path = MixedText}">
        </TextBlock>

ViewModel 部分:定义绑定路径属性。

    public string MixedText
    {
        get { return _mixedText; }
        set
        {
            _mixedText = value;
            OnPropertyChanged();
        }
    }
    string _mixedText;

MultipartTextHandler 类,我在其中实现 MixedText 解析和动态 xaml 注入模型准备。

class MultipartTextHandler
{
    public static IEnumerable<(int Index, Type Type, object Control, string Text, bool IsHyperlink)> CreateControls(string multipartText)
    {
        // 1. Return null if no multipart text is found. This will be just an ordinary text passed to a binding path.
        var multipartTextCollection = GetMultipartTextCollection(multipartText);
        if (!multipartTextCollection.Any())
            return Enumerable.Empty<(int Index, Type Type, object Control, string Text, bool IsHyperlink)>();

        var result = new List<(int Index, Type Type, object Control, string Text, bool IsHyperlink)>();

        // 2. Process multipart texts that have Hyperlink content.
        foreach (var e in multipartTextCollection.Where(x => x.Hyperlink != null))
        {
            var hyperlink = new Hyperlink { NavigateUri = new Uri(e.Hyperlink) };
            hyperlink.Click += (sender, e1) => Process.Start(new ProcessStartInfo(new Uri(e.Hyperlink).ToString()));
            hyperlink.Inlines.Add(new Run { Text = e.Text });
            result.Add((Index: e.Index, Type: typeof(Hyperlink), Control: hyperlink, Text: e.Text, IsHyperlink: true));
        }

        // 3. Process multipart texts that do not have Hyperlink content.
        foreach (var e in multipartTextCollection.Where(x => x.Hyperlink == null))
        {
            var inline = new Run { Text = e.Text };
            result.Add((Index: e.Index, Type: typeof(Inline), Control: inline, Text: e.Text, IsHyperlink: false));
        }

        return result.OrderBy(x => x.Index);
    }

    /// <summary>
    /// Returns list of Inline and Hyperlink segments.
    /// Parameter sample:
    /// "Please visit the Microsoft [site|https://www.microsoft.com/en-us/windows/windows-7-end-of-life-support-information], and download the Windows 7 SP1, complete the SP1 installation then re-run the installer again. Go to [roblox|https://www.roblox.com] site to relax a bit like my son ☀."
    /// </summary>
    /// <param name="multipartText">See sample on comment</param>
    static IEnumerable<(int Index, string Text, string Hyperlink)> GetMultipartTextCollection(string multipartText)
    {
        // 1. Make sure we have a url string in parameter argument.
        if (!ContainsURL(multipartText))
            return Enumerable.Empty<(int Index, string Text, string Hyperlink)>();

        // 2a. Make sure format of url link fits to our parsing schema.
        if (multipartText.Count(x => x == '[' || x == ']') % 2 != 0)
            return Enumerable.Empty<(int Index, string Text, string Hyperlink)>();

        // 2b. Make sure format of url link fits to our parsing schema.
        if (multipartText.Count(x => x == '|') != multipartText.Count(x => x == '[' || x == ']') / 2)
            return Enumerable.Empty<(int Index, string Text, string Hyperlink)>();

        var result = new List<(int Index, string Text, string Hyperlink)>();

        // 3. Split to Inline and Hyperlink segments.
        var multiParts = multipartText.Split(new char[] { '[', ']' }, StringSplitOptions.RemoveEmptyEntries);
        foreach (var part in multiParts)
        {
            // Hyperlink segment must contain inline and Hyperlink splitter checked in step 2b.
            if (part.Contains('|'))
            {
                // 4a. Split the hyperlink segment of the overall multipart text to Hyperlink's inline
                // and Hyperlink "object" contents. Note that the 1st part is the text that will be
                // visible inline text with 2nd part that will have the url link "under."
                var hyperPair = part.Split(new char[] { '|' }, StringSplitOptions.RemoveEmptyEntries);

                // 4b. Add hyperlink record to the return list: Make sure we keep the order in which 
                // these values are set at multipartText. Note that Hyperlink's inline, and Hyperlink 
                // url texts are added to Text: and Hyperlink: properties separately.
                result.Add((Index: result.Count + 1, Text: hyperPair[0], Hyperlink: hyperPair[1]));
            }
            else
            {
                // 5. This text will be an inline element either before or after the hyperlink element.
                // So, Hyperlink parameter we will set null to later process differently.
                result.Add((Index: result.Count + 1, Text: part, Hyperlink: null));
            }
        }

        return result;
    }

    /// <summary>
    /// Returns true if a text contains a url string (pattern).
    /// </summary>
    /// <param name="Text"></param>
    /// <returns></returns>
    static bool ContainsURL(string Text)
    {
        var pattern = @"([a-zA-Z\d]+:\/\/)?((\w+:\w+@)?([a-zA-Z\d.-]+\.[A-Za-z]{2,4})(:\d+)?(\/)?([\S]+))";
        var regex = new Regex(pattern, RegexOptions.Compiled | RegexOptions.IgnoreCase);
        return regex.IsMatch(Text);
    }
}

代码隐藏的东西。

  1. 在视图构造函数内部:

    this.DataContextChanged += MessageBoxView_DataContextChanged;

  2. MessageBoxView_DataContextChanged 实现。

     private void MessageBoxView_DataContextChanged(对象发送者,DependencyPropertyChangedEventArgs e)
     {
         var viewModel = (MessageBoxViewModel)e.NewValue;
         var mixText = viewModel.MixedText;
         var Components = MultipartTextHandler.CreateControls(mixedText);
         this.HyperlinkContent.Inlines.Clear();
         this.HyperlinkContent.Text = null;
         foreach(组件中的var内容)
         {
             if (content.Type == typeof(Inline))
                 this.HyperlinkContent.Inlines.Add(new Run { Text = content.Text });
    
             else if (content.Type == typeof(超链接))
                 this.HyperlinkContent.Inlines.Add((Hyperlink)content.Control);
         }
     }
    

来自我的控制台应用程序的用法

    static void Test()
    {
        var viewModel = new MessageBox.MessageBoxViewModel()
        {
            MixedText = "Please visit the Microsoft [site|https://www.microsoft.com/en-us/windows/windows-7-end-of-life-support-information], and download the Windows 7 SP1, complete the SP1 installation then re-run the installer again. Go to [roblox|https://www.roblox.com] site to relax a bit like my son \u263A.",
        };
        var view = new MessageBox.MessageBoxView();
        view.DataContext = viewModel; // Here is where all fun stuff happens

        var application = new System.Windows.Application();
        application.Run(view);

        Console.WriteLine("Hello World!");
    }

实际的对话框显示视图:

在此处输入图像描述

Most recently I had a similar task to solve, namely; having unlimited number of url links inserted to a custom message box text content, and have a binding path to this text.
I decided to post my implementation here seeing that this thread had some evolution of different great ideas... Here is my solution:

The concept:
The flow of xaml TextBlock content:

<TextBlock>
  ...
  <Inline>
  <Hyperlink <Inline>>
  <Inline>
  <Hyperlink <Inline>>
  ...
  1. My x:Name=MixedText TextBlock element receives its value as a single text formated as:

"...some text here...[link-text|url-link]...some other text here... etc."

Sample:

"Please visit the Microsoft [site|https://www.microsoft.com/en-us/windows/windows-7-end-of-life-support-information], and download the Windows 7 SP1, complete the SP1 installation then re-run the installer again. Go to [roblox|https://www.roblox.com] site to relax a bit like my son \u263A."

  1. I do my parsing and all elements' injection to my MixedText TextBlock element at the DataContextChanged event.

The xaml part: Defining the binding path (MixedText).
...

         <TextBlock Grid.Row="3" Grid.Column="1" 
                    x:Name="HyperlinkContent" 
                    TextWrapping="Wrap"
                    VerticalAlignment="Top"
                    HorizontalAlignment="Left"
                    Text="{Binding Path = MixedText}">
        </TextBlock>

The ViewModel part: Defining the binding path property.

    public string MixedText
    {
        get { return _mixedText; }
        set
        {
            _mixedText = value;
            OnPropertyChanged();
        }
    }
    string _mixedText;

The MultipartTextHandler class where I implement the MixedText parsing and dynamic xaml injection model preparation.

class MultipartTextHandler
{
    public static IEnumerable<(int Index, Type Type, object Control, string Text, bool IsHyperlink)> CreateControls(string multipartText)
    {
        // 1. Return null if no multipart text is found. This will be just an ordinary text passed to a binding path.
        var multipartTextCollection = GetMultipartTextCollection(multipartText);
        if (!multipartTextCollection.Any())
            return Enumerable.Empty<(int Index, Type Type, object Control, string Text, bool IsHyperlink)>();

        var result = new List<(int Index, Type Type, object Control, string Text, bool IsHyperlink)>();

        // 2. Process multipart texts that have Hyperlink content.
        foreach (var e in multipartTextCollection.Where(x => x.Hyperlink != null))
        {
            var hyperlink = new Hyperlink { NavigateUri = new Uri(e.Hyperlink) };
            hyperlink.Click += (sender, e1) => Process.Start(new ProcessStartInfo(new Uri(e.Hyperlink).ToString()));
            hyperlink.Inlines.Add(new Run { Text = e.Text });
            result.Add((Index: e.Index, Type: typeof(Hyperlink), Control: hyperlink, Text: e.Text, IsHyperlink: true));
        }

        // 3. Process multipart texts that do not have Hyperlink content.
        foreach (var e in multipartTextCollection.Where(x => x.Hyperlink == null))
        {
            var inline = new Run { Text = e.Text };
            result.Add((Index: e.Index, Type: typeof(Inline), Control: inline, Text: e.Text, IsHyperlink: false));
        }

        return result.OrderBy(x => x.Index);
    }

    /// <summary>
    /// Returns list of Inline and Hyperlink segments.
    /// Parameter sample:
    /// "Please visit the Microsoft [site|https://www.microsoft.com/en-us/windows/windows-7-end-of-life-support-information], and download the Windows 7 SP1, complete the SP1 installation then re-run the installer again. Go to [roblox|https://www.roblox.com] site to relax a bit like my son ☀."
    /// </summary>
    /// <param name="multipartText">See sample on comment</param>
    static IEnumerable<(int Index, string Text, string Hyperlink)> GetMultipartTextCollection(string multipartText)
    {
        // 1. Make sure we have a url string in parameter argument.
        if (!ContainsURL(multipartText))
            return Enumerable.Empty<(int Index, string Text, string Hyperlink)>();

        // 2a. Make sure format of url link fits to our parsing schema.
        if (multipartText.Count(x => x == '[' || x == ']') % 2 != 0)
            return Enumerable.Empty<(int Index, string Text, string Hyperlink)>();

        // 2b. Make sure format of url link fits to our parsing schema.
        if (multipartText.Count(x => x == '|') != multipartText.Count(x => x == '[' || x == ']') / 2)
            return Enumerable.Empty<(int Index, string Text, string Hyperlink)>();

        var result = new List<(int Index, string Text, string Hyperlink)>();

        // 3. Split to Inline and Hyperlink segments.
        var multiParts = multipartText.Split(new char[] { '[', ']' }, StringSplitOptions.RemoveEmptyEntries);
        foreach (var part in multiParts)
        {
            // Hyperlink segment must contain inline and Hyperlink splitter checked in step 2b.
            if (part.Contains('|'))
            {
                // 4a. Split the hyperlink segment of the overall multipart text to Hyperlink's inline
                // and Hyperlink "object" contents. Note that the 1st part is the text that will be
                // visible inline text with 2nd part that will have the url link "under."
                var hyperPair = part.Split(new char[] { '|' }, StringSplitOptions.RemoveEmptyEntries);

                // 4b. Add hyperlink record to the return list: Make sure we keep the order in which 
                // these values are set at multipartText. Note that Hyperlink's inline, and Hyperlink 
                // url texts are added to Text: and Hyperlink: properties separately.
                result.Add((Index: result.Count + 1, Text: hyperPair[0], Hyperlink: hyperPair[1]));
            }
            else
            {
                // 5. This text will be an inline element either before or after the hyperlink element.
                // So, Hyperlink parameter we will set null to later process differently.
                result.Add((Index: result.Count + 1, Text: part, Hyperlink: null));
            }
        }

        return result;
    }

    /// <summary>
    /// Returns true if a text contains a url string (pattern).
    /// </summary>
    /// <param name="Text"></param>
    /// <returns></returns>
    static bool ContainsURL(string Text)
    {
        var pattern = @"([a-zA-Z\d]+:\/\/)?((\w+:\w+@)?([a-zA-Z\d.-]+\.[A-Za-z]{2,4})(:\d+)?(\/)?([\S]+))";
        var regex = new Regex(pattern, RegexOptions.Compiled | RegexOptions.IgnoreCase);
        return regex.IsMatch(Text);
    }
}

The Code-behind stuff.

  1. Inside the view constructor:

    this.DataContextChanged += MessageBoxView_DataContextChanged;

  2. The MessageBoxView_DataContextChanged implementation.

     private void MessageBoxView_DataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
     {
         var viewModel = (MessageBoxViewModel)e.NewValue;
         var mixedText = viewModel.MixedText;
         var components = MultipartTextHandler.CreateControls(mixedText);
         this.HyperlinkContent.Inlines.Clear();
         this.HyperlinkContent.Text = null;
         foreach (var content in components)
         {
             if (content.Type == typeof(Inline))
                 this.HyperlinkContent.Inlines.Add(new Run { Text = content.Text });
    
             else if (content.Type == typeof(Hyperlink))
                 this.HyperlinkContent.Inlines.Add((Hyperlink)content.Control);
         }
     }
    

The usage, from my console application.

    static void Test()
    {
        var viewModel = new MessageBox.MessageBoxViewModel()
        {
            MixedText = "Please visit the Microsoft [site|https://www.microsoft.com/en-us/windows/windows-7-end-of-life-support-information], and download the Windows 7 SP1, complete the SP1 installation then re-run the installer again. Go to [roblox|https://www.roblox.com] site to relax a bit like my son \u263A.",
        };
        var view = new MessageBox.MessageBoxView();
        view.DataContext = viewModel; // Here is where all fun stuff happens

        var application = new System.Windows.Application();
        application.Run(view);

        Console.WriteLine("Hello World!");
    }

The actual dialog display view:

enter image description here

鯉魚旗 2024-08-22 13:20:31
Imports System.Collections.ObjectModel
Imports System.Collections.Specialized

Public Class BindableTextBlock
Inherits TextBlock

Public Property InlineList As ObservableCollection(Of Inline)
    Get
        Return GetValue(InlineListProperty)
    End Get

    Set(ByVal value As ObservableCollection(Of Inline))
        SetValue(InlineListProperty, value)
    End Set
End Property

Public Shared ReadOnly InlineListProperty As DependencyProperty = _
                       DependencyProperty.Register("InlineList", _
                       GetType(ObservableCollection(Of Inline)), GetType(BindableTextBlock), _
                       New UIPropertyMetadata(Nothing, AddressOf OnInlineListPropertyChanged))

Private Shared Sub OnInlineListPropertyChanged(sender As DependencyObject, e As DependencyPropertyChangedEventArgs)
    Dim textBlock As BindableTextBlock = TryCast(sender, BindableTextBlock)
    Dim list As ObservableCollection(Of Inline) = TryCast(e.NewValue, ObservableCollection(Of Inline))
    If textBlock IsNot Nothing Then
        If list IsNot Nothing Then
            ' Add in the event handler for collection changed
            AddHandler list.CollectionChanged, AddressOf textBlock.InlineCollectionChanged
            textBlock.Inlines.Clear()
            textBlock.Inlines.AddRange(list)
        Else
            textBlock.Inlines.Clear()

        End If
    End If
End Sub

''' <summary>
''' Adds the items to the inlines
''' </summary>
''' <param name="sender"></param>
''' <param name="e"></param>
''' <remarks></remarks>
Private Sub InlineCollectionChanged(sender As Object, e As NotifyCollectionChangedEventArgs)
    Select Case e.Action
        Case NotifyCollectionChangedAction.Add
            Me.Inlines.AddRange(e.NewItems)
        Case NotifyCollectionChangedAction.Reset
            Me.Inlines.Clear()
        Case NotifyCollectionChangedAction.Remove
            For Each Line As Inline In e.OldItems
                If Me.Inlines.Contains(Line) Then
                    Me.Inlines.Remove(Line)
                End If
            Next
    End Select
End Sub

End Class

我认为您可能需要在 PropertyChanged 处理程序上添加一些额外的代码,以便在绑定集合已包含内容时初始化 textBlock.Inlines,并清除任何现有上下文。

Imports System.Collections.ObjectModel
Imports System.Collections.Specialized

Public Class BindableTextBlock
Inherits TextBlock

Public Property InlineList As ObservableCollection(Of Inline)
    Get
        Return GetValue(InlineListProperty)
    End Get

    Set(ByVal value As ObservableCollection(Of Inline))
        SetValue(InlineListProperty, value)
    End Set
End Property

Public Shared ReadOnly InlineListProperty As DependencyProperty = _
                       DependencyProperty.Register("InlineList", _
                       GetType(ObservableCollection(Of Inline)), GetType(BindableTextBlock), _
                       New UIPropertyMetadata(Nothing, AddressOf OnInlineListPropertyChanged))

Private Shared Sub OnInlineListPropertyChanged(sender As DependencyObject, e As DependencyPropertyChangedEventArgs)
    Dim textBlock As BindableTextBlock = TryCast(sender, BindableTextBlock)
    Dim list As ObservableCollection(Of Inline) = TryCast(e.NewValue, ObservableCollection(Of Inline))
    If textBlock IsNot Nothing Then
        If list IsNot Nothing Then
            ' Add in the event handler for collection changed
            AddHandler list.CollectionChanged, AddressOf textBlock.InlineCollectionChanged
            textBlock.Inlines.Clear()
            textBlock.Inlines.AddRange(list)
        Else
            textBlock.Inlines.Clear()

        End If
    End If
End Sub

''' <summary>
''' Adds the items to the inlines
''' </summary>
''' <param name="sender"></param>
''' <param name="e"></param>
''' <remarks></remarks>
Private Sub InlineCollectionChanged(sender As Object, e As NotifyCollectionChangedEventArgs)
    Select Case e.Action
        Case NotifyCollectionChangedAction.Add
            Me.Inlines.AddRange(e.NewItems)
        Case NotifyCollectionChangedAction.Reset
            Me.Inlines.Clear()
        Case NotifyCollectionChangedAction.Remove
            For Each Line As Inline In e.OldItems
                If Me.Inlines.Contains(Line) Then
                    Me.Inlines.Remove(Line)
                End If
            Next
    End Select
End Sub

End Class

I think you may need some additional code on the PropertyChanged handler, so to initialise the textBlock.Inlines if the bound collection already has content, and to clear any existing context.

你对谁都笑 2024-08-22 13:20:31

每个人都给出了很好的解决方案,但我遇到了类似的问题,在寻找解决方案后,我决定尝试直接绑定到默认内容。没有依赖属性。
抱歉,我的英语已经过时了...呵呵呵呵

[ContentProperty("Inlines")]
public partial class WindowControl : UserControl
{
    public InlineCollection Inlines { get => txbTitle.Inlines; }
}

好吧,让我们在您的 xaml 文件上使用它...

<local:WindowControl>
    .:: Register Logbook : Connected User - <Run Text="{Binding ConnectedUser.Name}"/> ::.
</local:WindowControl>

瞧!

这是因为它们绑定内联是不必要的,您可以修改另一个控件内容中的文本部分而无需绑定,这个解决方案对我有帮助。

Everyone given good solutions, but I had a similar problem and after hours looking for solutions I decide try directly bind to default content. Without Dependency Properties.
Sorry my obsolete english... hehehehe

[ContentProperty("Inlines")]
public partial class WindowControl : UserControl
{
    public InlineCollection Inlines { get => txbTitle.Inlines; }
}

Ok, lets use this on your xaml file...

<local:WindowControl>
    .:: Register Logbook : Connected User - <Run Text="{Binding ConnectedUser.Name}"/> ::.
</local:WindowControl>

And voila!

It's because they bind inlines is unnecessary, you can modify de parts of a text from another control contents without a binding, this solution help-me.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文