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>>
...
- 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."
- 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.
Inside the view constructor:
this.DataContextChanged += MessageBoxView_DataContextChanged;
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: