Custom WPF control with Unity failing to resolve - wpf

public class RichTextBoxExtended : RichTextBox
{
static RichTextBoxExtended()
{
//DefaultStyleKeyProperty.OverrideMetadata(typeof(RichTextBoxExtended), new FrameworkPropertyMetadata(typeof(RichTextBoxExtended)));
}
public byte[] Text
{
get { return (byte[])GetValue(TextProperty); }
set { SetValue(TextProperty, value); }
}
// Using a DependencyProperty as the backing store for Text. This enables animation, styling, binding, etc...
public static readonly DependencyProperty TextProperty =
DependencyProperty.Register("Text", typeof(byte[]), typeof(RichTextBoxExtended), new UIPropertyMetadata(null, new PropertyChangedCallback(TextChangedCallback)));
private static void TextChangedCallback(DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
RichTextBoxExtended richTextBoxExtended = obj as RichTextBoxExtended;
richTextBoxExtended.ChangeText(e);
}
private void ChangeText(DependencyPropertyChangedEventArgs e)
{
//clear out any formatting properties
TextRange range = new TextRange(base.Document.ContentStart, base.Document.ContentEnd);
range.ClearAllProperties();
//load bytes into stream, load stream into range
MemoryStream stream = new MemoryStream(e.NewValue as byte[]);
range.Load(stream, DataFormats.Rtf);
}
}
Above is the custom control...
XAML implementing custom control...
<Grid>
<controls:RichTextBoxExtended x:Name="Document" Text="{Binding Path=File}">
</controls:RichTextBoxExtended>
</Grid>
Associated VM...
public class FileViewerViewModel : AViewModel
{
private byte[] _file = null;
public FileViewerViewModel(ILoggerFacade logger)
{
}
/// <summary>
/// Gets or sets the <seealso cref="DataFormats.Rtf"/> file representation as a <seealso cref="byte[]"/>
/// </summary>
public byte[] File
{
get
{
return _file;
}
set
{
_file = value;
RaiseChanged(() => this.File);
}
}
}
Finally..if I call...
FileViewerView view = _container.Resolve<FileViewerView>();
It fails.
Resolution of the dependency failed, type = "cyos.infrastructure.Views.FileViewerView", name = "". Exception message is: The current build operation (build key Build Key[cyos.infrastructure.Views.FileViewerView, null]) failed: Object reference not set to an instance of an object. (Strategy type BuildPlanStrategy, index 3)
If I remove the binding from within the XAML...
<Grid>
<controls:RichTextBoxExtended x:Name="Document">
</controls:RichTextBoxExtended>
</Grid>
Everything works without a hitch...no problems at all...ideas?
EDIT:
Switched to creating a new instance, bypassing Unity. Issue is still in the same place, exception at construction of FileViewerView at InitializeComponent() with "Object reference not set to an instance of an object."
at System.Windows.Markup.BamlRecordReader.ProvideValueFromMarkupExtension(MarkupExtension markupExtension, Object obj, Object member)
at System.Windows.Markup.BamlRecordReader.ReadPropertyArrayEndRecord()
at System.Windows.Markup.BamlRecordReader.ReadRecord(BamlRecord bamlRecord)
at System.Windows.Markup.BamlRecordReader.Read(Boolean singleRecord)
at System.Windows.Markup.TreeBuilderBamlTranslator.ParseFragment()
at System.Windows.Markup.TreeBuilder.Parse()
at System.Windows.Markup.XamlReader.LoadBaml(Stream stream, ParserContext parserContext, Object parent, Boolean closeStream)
at System.Windows.Application.LoadComponent(Object component, Uri resourceLocator)
at cyos.infrastructure.Views.FileViewerView.InitializeComponent() in c:\Documents and Settings\amciver\My Documents\dev\cyos\cyos\cyos.infrastructure\Views\FileViewerView.xaml:line 1
at cyos.infrastructure.Views.FileViewerView..ctor(FileViewerViewModel viewModel) in now...

I don't know what the problem with array, but List<byte> works.

Related

Binding to a custom control in edit mode [duplicate]

When I set the value of IsClosed during runtime, OnIsClosedChanged() is called fine.
However, the Designer sets the value of the property but does not call the OnIsClosedChanged().
public static DependencyProperty IsClosedProperty = DependencyProperty.Register("IsClosed", typeof(bool), typeof(GroupBox), new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.AffectsRender));
public bool IsClosed {
get {
return (bool)this.GetValue(IsClosedProperty);
}
set {
if ((bool)this.GetValue(IsClosedProperty) == value)
return;
this.SetValue(IsClosedProperty, value);
OnIsClosedChanged();
}
}
private void OnIsClosedChanged() {
_rowDefContent.Height = new GridLength((IsClosed ? 0 : 1), GridUnitType.Star);
}
Obviously IsClosed is not modified by the Designer and only IsClosedProperty receives the xaml change.
My question is: How can I run IsClosed after the value has been modified in the Designer. Or at least add some logic to the non-runtime changes.
You would have to register a PropertyChangedCallback with property metadata.
The reason is that dependency properties set in XAML or by bindings or some other source do not invoke the CLR wrapper (the setter method). The reason is explained in the XAML Loading and Dependency Properties article on MSDN:
For implementation reasons, it is computationally less expensive to
identify a property as a dependency property and access the property
system SetValue method to set it, rather than using the property
wrapper and its setter.
...
Because the current WPF implementation of the XAML processor behavior
for property setting bypasses the wrappers entirely, you should not
put any additional logic into the set definitions of the wrapper for
your custom dependency property. If you put such logic in the set
definition, then the logic will not be executed when the property is
set in XAML rather than in code.
Your code should look like this:
public static readonly DependencyProperty IsClosedProperty =
DependencyProperty.Register(
"IsClosed", typeof(bool), typeof(GroupBox),
new FrameworkPropertyMetadata(false,
FrameworkPropertyMetadataOptions.AffectsRender,
(o, e) => ((GroupBox)o).OnIsClosedChanged()));
public bool IsClosed
{
get { return (bool)GetValue(IsClosedProperty); }
set { SetValue(IsClosedProperty, value); }
}
private void OnIsClosedChanged()
{
_rowDefContent.Height = new GridLength((IsClosed ? 0 : 1), GridUnitType.Star);
}
Found the answer myself now. ValidateValueCallback comes really close! (as Alex K has pointed out) But it is a static method and I don't get any reference to the instance which has been changed. The key is to use a PropertyChangedCallback in FrameworkPropertyMetadata which is also an argument passed to the Property.Register method.
See:
public static DependencyProperty IsClosedProperty = DependencyProperty.Register("IsClosed", typeof(bool), typeof(GroupBox), new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.AffectsRender, new PropertyChangedCallback(OnIsClosedChangedPCC)));
public bool IsClosed {
get {
return (bool)this.GetValue(IsClosedProperty);
}
set {
this.SetValue(IsClosedProperty, value);
OnIsClosedChanged();
}
}
private static void OnIsClosedChangedPCC(DependencyObject d, DependencyPropertyChangedEventArgs e) {
GroupBox current = (GroupBox)d;
current.IsClosed = current.IsClosed;
}
private void OnIsClosedChanged() {
_rowDefContent.Height = new GridLength((IsClosed ? 0 : 1), GridUnitType.Star);
}
That does now re-set the IsClosedValue which triggers the OnIsClosedChanged to run.
Thank's for your help guys!

In WPF, how do I copy a binding from a TextBlock to custom collection?

Problem: I have a ListBox with TextBlocks whos Text property are bound to different properties. I wish to drag the TextBlock onto a OxyPlot and have the plot create a new LineSeries with a collection that should be bound to the same binding as for the TextBlock (is this making sense?)
I have derived a class from TextBlock to handle the OnMouseMove event like this:
protected override void OnMouseMove(MouseEventArgs e)
{
base.OnMouseMove(e);
if (CanDrag && (e.LeftButton == MouseButtonState.Pressed))
{
// Make sure we have a data binding
BindingExpression binding = GetBindingExpression(TextProperty);
if(binding == null)
{ return; }
// Package the data.
DataObject data = new DataObject();
data.SetData("DragListText.Binding", binding);
// Inititate the drag-and-drop operation.
DragDrop.DoDragDrop(this, data, DragDropEffects.Copy);
}
}
Also I have derived a class from Oxy.Plot that handles the OnDrop:
protected override void OnDrop(DragEventArgs e)
{
base.OnDrop(e);
// DataObject must contain a DragListText.Binding object
if (e.Data.GetDataPresent("DragListText.Binding"))
{
BindingExpression binding = e.Data.GetData("DragListText.Binding") as BindingExpression;
AddSeries(binding);
}
e.Handled = true;
}
The AddSeries function does the following:
public void AddSeries(BindingExpression binding)
{
plot1 = new PlotCollection();
LineSeries newSeries = new LineSeries();
newSeries.ItemsSource = plot1.Collection;
Series.Add(newSeries);
}
And lastly the PlotCollection is defined as:
public class PlotCollection : DependencyObject
{
public ObservableCollection<DataPoint> Collection;
public static DependencyProperty ValueProperty = DependencyProperty.Register("Value", typeof(double), typeof(PlotCollection), new PropertyMetadata(0.0, new PropertyChangedCallback(OnValueChanged)));
private static void OnValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{ ((PlotCollection)d).AddLast(); }
public double Value
{
get { return (double)GetValue(ValueProperty); }
set { SetValue(ValueProperty, value); }
}
public PlotCollection()
{
Collection = new ObservableCollection<DataPoint>();
}
protected void AddLast()
{
Collection.Add(new DataPoint(OxyPlot.Axes.DateTimeAxis.ToDouble(DateTime.Now), Value));
}
}
So my question is this: How do I create a binding on PlotCollection.Value that matches the one from the TextBlock.Text?
In your AddSeries method, try adding this line of code:
BindingOperations.SetBinding(plot1, PlotCollection.ValueProperty, binding.ParentBinding);
Found out the problem,
I needed to add a PropertyChangedCallback to the ValueProperty declaration, like this:
public static DependencyProperty ValueProperty = DependencyProperty.Register("Value", typeof(double), typeof(DynamicSeries), new PropertyMetadata(0.0, OnValueChanged));
And then handle the property changes in the callback method:
private static void OnValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
PlotCollection ds = (PlotCollection)d;
ds.AppendValue((double)e.NewValue);
}
I guess that I have misunderstood how the Value property works?!
Thanks for taking the time to try and help me...

Catel and setting usercontrol's DataContext

I've tried to find a solution myself but I was not able, for some reason the DataContext is not correctly set up in the usercontrol's viewmodel
The idea is to have a single usercontrol that permits to perform a query on a fixed collection and allows the user to drop a treeviewitem that holds an item of the collection (in case the user have the treeview open)
In my main view I've defined :
<views:PortfolioChooserView x:Name="PortfolioChooserView" DataContext="{Binding PortfolioCompleteBox}" Height="25" LoadDefaultValue="True" />
Where PortfolioCompleteBox is a ViewModel defined in the MainViewModel as
public PortfolioChooserViewModel PortfolioCompleteBox
{
get { return GetValue<PortfolioChooserViewModel>(PortfolioChooserViewModelProperty); }
set { SetValue(PortfolioChooserViewModelProperty, value); }
}
public static readonly PropertyData PortfolioChooserViewModelProperty = RegisterProperty("PortfolioCompleteBox", typeof(PortfolioChooserViewModel));
public MainViewModel(ICreditLimitRepository creditLimitRepository, IDynamicContainer dynamicContainer)
{
this.creditLimitRepository = creditLimitRepository;
this.dynamicContainer = dynamicContainer;
LoadCreditLimitsCommand = new Command<object>(OnLoadCreditLimitsExecute, (() => OnLoadCreditLimitsCanExecute));
var viewModelFactory = this.GetServiceLocator().ResolveType<IViewModelFactory>();
PortfolioCompleteBox = viewModelFactory.CreateViewModel<PortfolioChooserViewModel>(null);
Model = new FiltersLoadModel();
}
My problem is that on the PortFolioChooserView I've the DataContext set to null (and I got 2 calls to the PortFolioChooserViewModel, one from the MainViewModel and the other one from the PortFolioChooserView's viewmodel locator)
public partial class PortfolioChooserView
{
private PortfolioChooserViewModel viewModel;
readonly bool isFirstLoad = true;
/// <summary>
/// Initializes a new instance of the <see cref="PortfolioChooserView"/> class.
/// </summary>
///
public PortfolioChooserView()
{
InitializeComponent();
if (isFirstLoad)
{
PortfolioCompleteBox.AllowDrop = true;
DragDropManager.AddPreviewDragOverHandler(PortfolioCompleteBox, OnElementDragOver);
DragDropManager.AddDropHandler(PortfolioCompleteBox, OnElementDrop);
isFirstLoad = false;
this.Loaded += PortfolioChooserView_Loaded;
this.DataContextChanged += PortfolioChooserView_DataContextChanged;
}
}
void PortfolioChooserView_DataContextChanged(object sender, System.Windows.DependencyPropertyChangedEventArgs e)
{
int t = 0;
}
void PortfolioChooserView_Loaded(object sender, System.Windows.RoutedEventArgs e)
{
viewModel = (PortfolioChooserViewModel)this.DataContext;
}
private void OnElementDragOver(object sender, Telerik.Windows.DragDrop.DragEventArgs e)
{
var options = Telerik.Windows.DragDrop.DragDropPayloadManager.GetDataFromObject(e.Data, TreeViewDragDropOptions.Key) as TreeViewDragDropOptions;
if (options != null)
{
var visual = options.DragVisual as TreeViewDragVisual;
if (visual != null) visual.IsDropPossible = true;
}
e.Handled = true;
}
private void OnElementDrop(object sender, Telerik.Windows.DragDrop.DragEventArgs e)
{
var context = ((IPortfolioAutoComplete)this.DataContext);
context.SetPortfolioAutoCompleteBox(e);
}
public static readonly DependencyProperty LoadDefaultValueProperty = DependencyProperty.Register(
"LoadDefaultValue", typeof(bool), typeof(PortfolioChooserView), new PropertyMetadata(default(bool)));
public bool LoadDefaultValue
{
get { return (bool)GetValue(LoadDefaultValueProperty); }
set { SetValue(LoadDefaultValueProperty, value); }
}
}
What am I doing wrong?
Thanks
Don't try to manage your own vm's
Catel will automatically accept a parent-vm as it's own vm as long as they are compatible. You don't need to handle this manually in your view loading in the view.
Instead of creating a VM in the parent VM, use a model only (so the vm only cares about what the VM itself should do). Then set the DC of the PortfolioChooserView to the model. Then the vm of the child view can accept the model in the ctor and be managed on it's own.
There are much better ways to communicate between vm's then trying to micro-manage like you are doing now. As always, see the docs.

Binding is not working for the first time in WPF

I have the following code in my sample.
public string MyProperty
{
get { return (string)GetValue(MyPropertyProperty); }
set { SetValue(MyPropertyProperty, value); }
}
public static readonly DependencyProperty MyPropertyProperty = DependencyProperty.Register("MyProperty", typeof(string), typeof(MainWindow), new PropertyMetadata("Hello"));
private TestClass1 test;
private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
Binding binding = new Binding
{
Path = new PropertyPath("MyProperty"),
Mode = BindingMode.TwoWay,
UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged
};
test.SetValueBinding(binding);
test.DataContext = this;
Console.WriteLine(test.Value);
}
public class TestClass1 : FrameworkElement
{
public object Value
{
get { return (object)GetValue(ValueProperty); }
set { SetValue(ValueProperty, value); }
}
public static readonly DependencyProperty ValueProperty = DependencyProperty.Register("Value", typeof(object), typeof(TestClass1), new PropertyMetadata(null));
public void SetValueBinding(Binding binding)
{
this.SetBinding(ValueProperty, binding);
}
}
After binding is set, if i access test.Value, it returns null for the first time. After that,if i access it (from another method) it returns correct value "Hello".
But i do not know first time why binding is not working and null value is returned? any suggestion?
Thanks in advance.
It's not that you can't get the value for the first time. It's that you can't get it so fast. When binding is set at the time the DataContext is NULL, the source is resolved in a deferred way, perhaps later by the UI thread. You ask for value just after the binding is set, too early to get valid results.
You should switch lines in your code to:
test.DataContext = this;
test.SetValueBinding(binding);
This way the binding gets the value immediately from DataContext in the first instruction.

Data binding the 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?
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);
}
}
}
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.
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.
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
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);
}
}
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()));
}
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()));
}
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 &#x2600."
/// </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:
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.
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.

Resources