WPF: Drawing on top of a TextBlock - wpf

I want to be able to draw on to the top of a TextBlock, and have found a way to do this, but i cannot remove the drawing once it is there. Here is the code.
public class DerivedTextBlock : TextBlock {
public Boolean DrawExtra {
get { return (Boolean)GetValue(DrawExtraProperty); }
set { SetValue(DrawExtraProperty, value); }
}
// Using a DependencyProperty as the backing store for DrawExtra. This enables animation, styling, binding, etc...
public static readonly DependencyProperty DrawExtraProperty =
DependencyProperty.Register("DrawExtra", typeof(Boolean), typeof(DerivedTextBlock), new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.AffectsRender | FrameworkPropertyMetadataOptions.AffectsArrange));
public DrawingVisual DrawingVisual { get; set; }
public DerivedTextBlock() {
DrawingVisual = this.CreateDrawingVisualRectangle();
}
protected override int VisualChildrenCount {
get {
//if we want to draw our extra info, add one to
// our visualChildrenCount, usually with a textblock it is 0
if (DrawExtra) {
return base.VisualChildrenCount + 1;
}
else {
return base.VisualChildrenCount;
}
}
}
protected override Visual GetVisualChild(int index) {
return DrawingVisual;
}
// Create a DrawingVisual that contains a rectangle.
private DrawingVisual CreateDrawingVisualRectangle() {
DrawingVisual drawingVisual = new DrawingVisual();
// Retrieve the DrawingContext in order to create new drawing content.
DrawingContext drawingContext = drawingVisual.RenderOpen();
// Create a rectangle and draw it in the DrawingContext.
Rect rect = new Rect(new Point(10.0, 0), new Size(10.0 / 2.0, 10));
drawingContext.DrawRectangle(Brushes.LightBlue, (Pen)null, rect);
// Persist the drawing content.
drawingContext.Close();
return drawingVisual;
}
}
Reason I want to do this: We have a datagrid with a lot of cells, each cell displaying text. we show some validation information on the cells and we do this by using a template with a textblock and some paths hosten in a grid. the overhead of this adds extra elements to the visual tree and when we have to redraw (on loading, switching windows or on a sort) it takes a lot longer the more elements in the visual tree. when it is just a textblock it is about 1/3 - 1/2 faster than having the control with a grid. So we would like to draw our validation stuff right on top of the textbox.

Your problems are:
GetVisualChild() should return base.GetVisualChild(index) except when index==base.VisualChildrenCount.
You forgot to call AddVisualChild() when DrawingExtra becomes true or DrawingVisual changes
You forgot to call RemoveVisualChild() when DrawingExtra becomes false or DrawingVisual changes
You can fix #2 and #3 by setting a PropertyChangedCallback on DrawingExtra and adding code to the setter of DrawingVisual.
Explanation: It is the AddVisualChild() call that actually adds the visual to the tree. What is happening is that your visual is being found and displayed "accidentally" because of your error in GetVisualChild(), but it is not being properly linked into the visual tree so you'll encounter many problems.
Update
I edited your code as described above, and it worked perfectly. Here are the changes:
...
{
PropertyChangedCallback = (obj, e) =>
{
var textBlock = (DerivedTextBlock)obj;
if((bool)e.OldValue) textBlock.RemoveVisualChild(textBlock.DrawingVisual);
if((bool)e.NewValue) textBlock.AddVisualChild(textBlock.DrawingVisual);
}
});
public DrawingVisual DrawingVisual
{
get { return _drawingVisual; }
set
{
if(DrawExtra) RemoveVisualChild(_drawingVisual);
_drawingVisual = value;
if(DrawExtra) AddVisualChild(_drawingVisual);
}
}
private DrawingVisual _drawingVisual;
...
protected override int VisualChildrenCount
{
get { return base.VisualChildrenCount + (DrawExtra ? 1 : 0); }
}
protected override Visual GetVisualChild(int index)
{
return index==base.VisualChildrenCount ? DrawingVisual : base.GetVisualChild(index);
}

Related

ItemsControl children return NAN when asking for Canvas.GetLeft

I have a very simple WPF application that renders simple shapes in a canvas:
The blue squares are ItemsControl and the red circles are Controls
The following step in my application is adding connection lines between the shapes. The shaphes will be moved and I want the connections to be automatically moved. I readed about how to do it adding connection bindings.
All worked fine with canvas direct children (container), but if I want to connect the nodes, it does not work. It seems that if I don't call Canvas.SetLeft() and Canvas.SetTop() explicitily, then Canvas.GetLeft() and Canvas.GetTop() return NAN.
How should I proceed?
Should I implement a mechanism to get all objects placed in my canvas, so I always can calculate Canvas.GetLeft() over all of them?
Should I proceed in another way?
Source code and screenshot
This is the source code of the example. You can find here the complete example:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
Container container1 = new Container() { Width = 100, Height = 100 };
Node node1 = new Node() { Width = 50, Height = 50 };
container1.Items.Add(node1);
Container container2 = new Container() { Width = 100, Height = 100 };
Node node2 = new Node() { Width = 50, Height = 50 };
container2.Items.Add(node2);
Canvas.SetLeft(container2, 200);
myCanvas.Children.Add(container1);
myCanvas.Children.Add(container2);
}
}
class Container : ItemsControl
{
protected override void OnRender(DrawingContext drawingContext)
{
drawingContext.DrawRectangle(
Brushes.Blue, null, new Rect(0, 0, this.Width, this.Height));
}
}
class Node : Control
{
protected override void OnRender(DrawingContext drawingContext)
{
drawingContext.DrawEllipse(
Brushes.Red, null,
new Point(Width / 2, Height / 2), Width / 2, Height / 2);
}
}
This is how I implemented the connections between the shapes:
public Shape AddConnection(UIElement source, UIElement target)
{
Connector conn = new Connector();
conn.SetBinding(Connector.StartPointProperty,
CreateConnectorBinding(source));
conn.SetBinding(Connector.EndPointProperty,
CreateConnectorBinding(target));
return conn;
}
private MultiBinding CreateConnectorBinding(UIElement connectable)
{
// Create a multibinding collection and assign an appropriate converter to it
MultiBinding multiBinding = new MultiBinding();
multiBinding.Converter = new ConnectorBindingConverter();
// Create binging #1 to IConnectable to handle Left
Binding binding = new Binding();
binding.Source = connectable;
binding.Path = new PropertyPath(Canvas.LeftProperty);
multiBinding.Bindings.Add(binding);
// Create binging #2 to IConnectable to handle Top
binding = new Binding();
binding.Source = connectable;
binding.Path = new PropertyPath(Canvas.TopProperty);
multiBinding.Bindings.Add(binding);
// Create binging #3 to IConnectable to handle ActualWidth
binding = new Binding();
binding.Source = connectable;
binding.Path = new PropertyPath(FrameworkElement.ActualWidthProperty);
multiBinding.Bindings.Add(binding);
// Create binging #4 to IConnectable to handle ActualHeight
binding = new Binding();
binding.Source = connectable;
binding.Path = new PropertyPath(FrameworkElement.ActualHeightProperty);
multiBinding.Bindings.Add(binding);
return multiBinding;
}
The Connector object is very simple. It has a LineGeometry and exposes two DependencyProperties to calculate the start point and the end point.
public static readonly DependencyProperty StartPointProperty =
DependencyProperty.Register(
"StartPoint",
typeof(Point),
typeof(Connector),
new FrameworkPropertyMetadata(
new Point(0, 0),
FrameworkPropertyMetadataOptions.AffectsMeasure));
public static readonly DependencyProperty EndPointProperty =
DependencyProperty.Register(
"EndPoint",
typeof(Point),
typeof(Connector),
new FrameworkPropertyMetadata(
new Point(0, 0),
FrameworkPropertyMetadataOptions.AffectsMeasure));
Everything is so wrong I can't really answer the question without fixing things.
Your nodes and containers shouldn't be controls that use OnRender. There's a lot of expectations in WPF, and one expectation is that you use their controls. If you dig into Microsoft code, they have a lot of things hard-coded for their classes.
You should have data objects for Node and Container that have Connections. Container should have a list of children Nodes.
You'll use a DataTemplate or Style to actually implement the UI. That's where you do your bindings, but don't use a multibinding. Just bind to individual values themselves. If you need to evaluate, then you create ViewModel objects that perform these calculations for you. You don't do your construction code in converters.
Because you're using bindings to connect things and your "connectable" doesn't describe whether it's a node or container or both, I'm going to assume it can be both.
For example:
public interface IConnection
{
IConnectable A { get; set; }
IConnectable B { get; set; }
}
public class Connection : IConnection, Line
{
DependencyProperty AProperty = ...;
DependencyProperty BProperty = ...;
}
public class Node : IConnectable
{
DependencyProperty ConnectionProperty = ...;
}
public class Container : IConnectable
{
DependencyProperty ConnectionProperty = ...;
ObservableCollection<IConnectable> Children = ...;
}
public class ContainerView : IConnectable
{
DependencyProperty ConnectionPointProperty = ...;
DependencyProperty ConnectionProperty = ...;
void OnSizeChanged(...)
{
RecalcConnectionPoint();
}
void OnConnectionPointOtherChanged()
{
RecalcConnectionPoint();
}
void RecalcConnectionPoint()
{
if (Connection.A == this)
{
if (Connection.B.ConnectionPoint.Left < this.Left)
{
ConnectionPoint = new Point(Left, Top + Height/2);
}
else
{
ConnectionPoint = new Point(Right, Top + Height/2);
}
}
}
}
Then you would bind the properties that match up from your Model classes to your ViewModel classes. Then manipulating the data in your Model classes would update your View.
Your Styles for your Container and Nodes would decide how to draw them, so say one day you decide a Node should look like a Rectangle instead... You change a style and don't have to dig through OnRender code.
This is how you design WPF programs.
Other benefits.
If you were to put a "Connection UI Object" somewhere on the Container, you'd bind to it's point instead. You could use a Grid to align the ConnectionPointView, and then the ConnectionPoint would be updated automatically.

How to create Windows 8 style app bar in WPF?

I intended to create a Windows 8 Style App (Metro), but found out there is no support for using dual screen which is a demand for my app.
Now I am redesigning my app as a desktop application in WPF.
But I still like to mimic some nice design features from Windows 8 Apps.
One of the design features is the fly out bars typically used in a Windows 8 style app:
Bottom App bar for commands
Top Navigational bar
Right Charm that is common for all apps
The design they all have in common is a temporary flyout panel that is layered on top of the current window layout.
My question is: How can I create something similar in WPF?
I have no problem to create a main grid with a hidden bottom row that is made visible to display some common command buttons. But it would be nice to have it fly out on top of my standard layout, not squeeze it.
I know it is possible to open a new window on top of the current but that creates a bad code design and is hard to get nice looking. I would prefer to do it in the same window.
Cool question! I've actually done the charm bar fairly recently..
ideally what you need is something like
<Grid x:Name="LayoutRoot">
<Grid x:Name="Overlay" Panel.ZIndex="1000" Visibility="Collapsed">
<!-- This is where your slide out control is going to go -->
</Grid>
<!-- Use whatever layout you need -->
<ContentControl x:Name="MainContent" />
</Grid>
Now rather than squeezing the content - the Overlay grid will be on top of it similar to the charm bar! all with XAML
If you have anymore questions about this, give me a shout!
Edit; my Charm implementation - feel free to use for inspriation!
public class SlidePanel : ContentControl
{
static SlidePanel()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(SlidePanel), new FrameworkPropertyMetadata(typeof(SlidePanel)));
}
public SlidePanel()
{
EventManager.RegisterClassHandler(typeof(SlidePanel), SlidePanel.MouseEnterEvent,
new RoutedEventHandler(OnLocalMouseEnter));
EventManager.RegisterClassHandler(typeof(SlidePanel), SlidePanel.MouseLeaveEvent,
new RoutedEventHandler(OnLocalMouseLeave));
}
#region Mouse Handlers
private static void OnLocalMouseEnter(object sender, RoutedEventArgs e)
{
SetExpanded(sender, true);
}
private static void OnLocalMouseLeave(object sender, RoutedEventArgs e)
{
SetExpanded(sender, false);
}
private static void SetExpanded(object sender, bool expanded)
{
SlidePanel panel = sender as SlidePanel;
if (panel != null)
{
panel.IsExpanded = expanded;
}
}
#endregion Mouse Handlers
#region Panel Width
public double PanelWidth
{
get { return (double)GetValue(PanelWidthProperty); }
set { SetValue(PanelWidthProperty, value); }
}
// Using a DependencyProperty as the backing store for PanelWidth. This enables animation, styling, binding, etc...
public static readonly DependencyProperty PanelWidthProperty =
DependencyProperty.Register("PanelWidth", typeof(double), typeof(SlidePanel), new UIPropertyMetadata(5.0));
#endregion Panel Width
#region Closed Width
public double ClosedWidth
{
get { return (double)GetValue(ClosedWidthProperty); }
set { SetValue(ClosedWidthProperty, value); }
}
// Using a DependencyProperty as the backing store for ClosedWidth. This enables animation, styling, binding, etc...
public static readonly DependencyProperty ClosedWidthProperty =
DependencyProperty.Register("ClosedWidth", typeof(double), typeof(SlidePanel), new UIPropertyMetadata(5.0, new PropertyChangedCallback(OnClosedWidthChange)));
#endregion Closed Width
#region Expanded Property
public bool IsExpanded
{
get { return (bool)GetValue(IsExpandedProperty); }
set { SetValue(IsExpandedProperty, value); }
}
// Using a DependencyProperty as the backing store for IsExpanded. This enables animation, styling, binding, etc...
public static readonly DependencyProperty IsExpandedProperty =
DependencyProperty.Register("IsExpanded", typeof(bool), typeof(SlidePanel), new UIPropertyMetadata(false, new PropertyChangedCallback(OnExpandedChanged)));
#endregion Expanded Property
#region Property Changes
private static void OnExpandedChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (e.NewValue == e.OldValue)
return;
SlidePanel panel = d as SlidePanel;
if (panel == null)
return;
bool newVal = (bool)e.NewValue;
panel.IsExpanded = newVal;
bool expanded = (bool)panel.GetValue(IsExpandedProperty);
Storyboard widthAnimation = AnimationHelper.CreateDoubleAnimation<SlidePanel>(panel, expanded,
(p, a) =>
{
a.From = (double)p.GetValue(SlidePanel.ClosedWidthProperty);
a.To = (double)p.GetValue(SlidePanel.PanelWidthProperty);
},
(p, a) =>
{
a.From = (double)p.GetValue(SlidePanel.WidthProperty);
a.To = (double)p.GetValue(SlidePanel.ClosedWidthProperty);
}, new TimeSpan(0, 0, 0, 0, 300), WidthProperty);
Timeline opacity = AnimationHelper.DoubleAnimation(0.0, 1.0, expanded,
new TimeSpan(0, 0, 0, 0, 300), OpacityProperty);
Storyboard.SetTargetName(opacity, panel.Name);
Storyboard.SetTargetProperty(opacity, new PropertyPath(OpacityProperty));
widthAnimation.Children.Add(opacity);
widthAnimation.Begin(panel);
}
private static void OnClosedWidthChange(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
SlidePanel panel = d as SlidePanel;
if (panel != null)
panel.Width = (double)e.NewValue;
}
#endregion Property Changes
}
A little trick I found was to have the opacity set to 0 when it wasnt expanded but set the width to 10, this then allows the user to put the mouse at the side of the screen and then it will appear after a second or so..
cheers.
Edit - As requested.. AnimationHelper.
public class AnimationHelper
{
public static Timeline DoubleAnimation(double from, double to, bool modifier, TimeSpan duration, DependencyProperty property)
{
DoubleAnimation animation = new DoubleAnimation();
if (modifier)
{
animation.From = from;
animation.To = to;
}
else
{
animation.To = from;
animation.From = to;
}
animation.Duration = new Duration(duration);
return animation;
}
public static Storyboard CreateDoubleAnimation<T>(T control, bool modifier, double from, double to, TimeSpan duration, DependencyProperty property) where T : Control
{
return
AnimationHelper.CreateDoubleAnimation<T>(control, modifier,
(p, a) =>
{
a.From = from;
a.To = to;
},
(p, a) =>
{
a.From = to;
a.To = from;
}, duration, property);
}
public static Storyboard CreateDoubleAnimation<T>(T control, bool modifier, Action<T, DoubleAnimation> onTrue, Action<T, DoubleAnimation> onFalse, TimeSpan duration, DependencyProperty property) where T : Control
{
if (control == null)
throw new ArgumentNullException("control");
DoubleAnimation panelAnimation = new DoubleAnimation();
if (modifier)
{
if (onTrue != null)
onTrue.Invoke(control, panelAnimation);
}
else
{
if (onFalse != null)
onFalse.Invoke(control, panelAnimation);
}
panelAnimation.Duration = new Duration(duration);
Storyboard sb = new Storyboard();
Storyboard.SetTargetName(panelAnimation, control.Name);
Storyboard.SetTargetProperty(panelAnimation, new PropertyPath(property));
sb.Children.Add(panelAnimation);
return sb;
}
}

RibbonApplicationMenu: getting rid of the AuxiliaryPane

It so happened that the application I'm working on doesn't operate on documents, so there's no need in displaying the recently opened documents list in the application menu.
But - annoyingly - there are no properties readily available in the RibbonApplicationMenu class to hide the unused AuxiliaryPane (for which, curiously, the property does exist, but is marked as "internal").
Of course, I can just leave it there - but that's... untidy.
So, here's the solution I came up with.
Hope it will be helpful for anyone else :-)
The general idea is to subclass the RibbonApplicationMenu, find the template child corresponding to the menu's Popup, and overrule its Width (after a number of frustrating experiments it became evident that doing that neither for PART_AuxiliaryPaneContentPresenter nor for PART_FooterPaneContentPresenter - nor for the both - could achieve anything).
Well, without further ado, here's the code:
public class SlimRibbonApplicationMenu : RibbonApplicationMenu
{
private const double DefaultPopupWidth = 180;
public double PopupWidth
{
get { return (double)GetValue(PopupWidthProperty); }
set { SetValue(PopupWidthProperty, value); }
}
public static readonly DependencyProperty PopupWidthProperty =
DependencyProperty.Register("PopupWidth", typeof(double),
typeof(SlimRibbonApplicationMenu), new UIPropertyMetadata(DefaultPopupWidth));
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
this.DropDownOpened +=
new System.EventHandler(SlimRibbonApplicationMenu_DropDownOpened);
}
void SlimRibbonApplicationMenu_DropDownOpened(object sender, System.EventArgs e)
{
DependencyObject popupObj = base.GetTemplateChild("PART_Popup");
Popup popupPanel = (Popup)popupObj;
popupPanel.Width = (double)GetValue(PopupWidthProperty);
}
}
As a side note, I tried to find any way to resolve the desired width based on the max width of the ApplicationMenu's Items (rather than setting it explicitly through the DependencyProperty in XAML) - but to no avail.
Given my despise to "magic numbers", any suggestion on that will be deeply appreciated.
I know this has been a while, but I've got another solution to this. This one does not provide the Popup width property, instead a ShowAuxilaryPanel boolean. It then goes to Bind the width of the Popup, to the width of the menu item area of the menu.
public class SlimRibbonApplicationMenu : RibbonApplicationMenu
{
public bool ShowAuxilaryPanel
{
get { return (bool)GetValue(ShowAuxilaryPanelProperty); }
set { SetValue(ShowAuxilaryPanelProperty, value); }
}
public static readonly DependencyProperty ShowAuxilaryPanelProperty =
DependencyProperty.Register("ShowAuxilaryPanel", typeof(bool),
typeof(SlimRibbonApplicationMenu), new UIPropertyMetadata(true));
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
this.DropDownOpened += SlimRibbonApplicationMenu_DropDownOpened;
}
void SlimRibbonApplicationMenu_DropDownOpened(object sender, EventArgs e)
{
DependencyObject popupObj = base.GetTemplateChild("PART_Popup");
Popup panel = (Popup)popupObj;
var exp = panel.GetBindingExpression(Popup.WidthProperty);
if (!this.ShowAuxilaryPanel && exp == null)
{
DependencyObject panelArea = base.GetTemplateChild("PART_SubMenuScrollViewer");
var panelBinding = new Binding("ActualWidth")
{
Source = panelArea,
Mode = BindingMode.OneWay
};
panel.SetBinding(Popup.WidthProperty, panelBinding);
}
else if (this.ShowAuxilaryPanel && exp != null)
{
BindingOperations.ClearBinding(panel, Popup.WidthProperty);
}
}
}
worked for me
<telerik:ApplicationMenu RightPaneVisibility="Collapsed" >

Adding Text inside a Shape

I have a custom WPF control MyLine that should represent or not some text in its middle.
public class MyLine : Shape
{
public double X1, Y1, X2, Y2;
public bool IsTextDisplayed;
public string Caption;
protected override System.Windows.Media.Geometry DefiningGeometry
{
get
{
var geometryGroup = new GeometryGroup();
if (IsTextDisplayed)
{
// calculate text point
var midPoint = new Point((X1 + X2) / 2.0, (Y1 + Y2) / 2.0);
// add a TextBlock with the Caption text in that point
// ???
}
// Add line
geometryGroup.Children.Add(new LineGeometry(
new Point(X1, Y1), new Point(X2, Y2)));
return geometryGroup;
}
}
}
How should I add the a TextBlock (or Label) here?
I tried to add a FormattedText inside, but this is NOK, because it draws the text with the line fat brush and is impossible to read something.
EDIT
Adding a visual Child
public MyLine() : base()
{
textBlock = new System.Windows.Controls.TextBlock();
textBlock.Visibility = System.Windows.Visibility.Hidden;
this.AddVisualChild(textBlock);
}
protected override System.Windows.Media.Geometry DefiningGeometry
{
get
{
...
if (IsTextDisplayed)
{
var midPoint = new Point((X1 + X2) / 2.0, (Y1 + Y2) / 2.0);
string text = "some custom text";
Canvas.SetLeft(textBlock, midPoint.X);
Canvas.SetBottom(textBlock, midPoint.Y);
textBlock.Text = text;
this.textBlock.Visibility = System.Windows.Visibility.Visible;
}
else
{
this.textBlock.Visibility = System.Windows.Visibility.Hidden;
}
I don't see any label... :"/
EDIT2
Adding Adorner
public MyLine() : base()
{
this.Loaded += new RoutedEventHandler(MyLine_Loaded);
}
void MyLine_Loaded(object sender, RoutedEventArgs e)
{
AdornerLayer aLayer = AdornerLayer.GetAdornerLayer(this);
if (aLayer != null)
aLayer.Add(new TextAdorner(this));
}
class TextAdorner : Adorner
{
public TextAdorner(UIElement adornedElement) : base(adornedElement)
{ }
protected override void OnRender(DrawingContext drawingContext)
{
MyLine segment = (this.AdornedElement as MyLine);
if (segment != null && segment.IsLabelUsed)
{
Rect segmentBounds = new Rect(segment.DesiredSize);
FormattedText ft = new FormattedText(
"654 m", Thread.CurrentThread.CurrentCulture,
System.Windows.FlowDirection.LeftToRight,
new Typeface("Arial"), 12, Brushes.White);
drawingContext.DrawText(ft, segmentBounds.BottomRight);
}
}
}
Now, apparently the code never enters in the OnRender adorner method...
If you don't want the text displayed in the same brush as the line, you probably don't want to use a Shape as the base class, as the Geometry returned from DefiningGeometry is rendered in one brush. If you really want to use a Shape as the base class, you probably want to add the text as a visual child rather than add it to the existing Geometry.
If you aren't going to be using a lot of these, and can afford a slightly heavy-weight container, I'd suggest simply creating a UserControl based control that contains this Shape you created, and a text element like TextBox on a Canvas (for absolute positioning). Again, this is not going to be a great solution if you have hundreds or thousands of these, but if you only have tens of them, it is probably the easiest and quickest solution.
I'd use an adorner to draw the text on top of the line.
http://msdn.microsoft.com/en-us/library/ms746703.aspx

Highlight Search TextBlock

My goal is to create a custom TextBlock control that has a new dependency property, SearchText. This property will contain a regular expression. All occurrences of this regular expression in the text of the TextBlock will be highlighted using a custom style (another DP).
My current implementation involves clearing all of the Inline objects in the TextBlock's InlineCollection. I then fill the TextBlock with runs for unhighlighted text and runs for highlighted text with the style applied (this method does not support adding inlines directly to the TextBlock, instead TextBlock.TextProperty has to be used).
Works great, but sometimes I get a strange exception when trying to clear the Inlines: InvalidOperationException: "Cannot modify the logical children for this node at this time because a tree walk is in progress."
This problem seems to be related to this one. I am modifying the inlines in the TextChanged function, but I'm using a flag to avoid infinite recursive edits.
Any thoughts on how to architect this custom control? Is there a better way to do this? How do I get around this exception?
Thanks!
In my implementation, I solved this by just adding another dependency property, called OriginalText. When it's modified, I updated both the Text property and update the highlighting. Here's the code:
public class HighlightTextBlock : TextBlock
{
public string HighlightedText
{
get { return (string)GetValue(HighlightedTextProperty); }
set { SetValue(HighlightedTextProperty, value); }
}
public static readonly DependencyProperty HighlightedTextProperty =
DependencyProperty.Register("HighlightedText", typeof(string), typeof(HighlightTextBlock), new UIPropertyMetadata(string.Empty, UpdateHighlightEffect));
public static readonly DependencyProperty OriginalTextProperty = DependencyProperty.Register(
"OriginalText", typeof(string), typeof(HighlightTextBlock), new PropertyMetadata(default(string), OnOriginalTextChanged));
private static void OnOriginalTextChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
var block = ((HighlightTextBlock)obj);
block.Text = block.OriginalText;
block.UpdateHighlightEffect();
}
public string OriginalText
{
get { return (string)GetValue(OriginalTextProperty); }
set { SetValue(OriginalTextProperty, value); }
}
private static void UpdateHighlightEffect(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
if (!(string.IsNullOrEmpty(e.NewValue as string) && string.IsNullOrEmpty(e.OldValue as string)))
((HighlightTextBlock)sender).UpdateHighlightEffect();
}
private void UpdateHighlightEffect()
{
if (string.IsNullOrEmpty(HighlightedText)) return;
var allText = GetCompleteText();
Inlines.Clear();
var indexOfHighlightString = allText.IndexOf(HighlightedText, StringComparison.InvariantCultureIgnoreCase);
if (indexOfHighlightString < 0)
{
Inlines.Add(allText);
}
else
{
Inlines.Add(allText.Substring(0, indexOfHighlightString));
Inlines.Add(new Run()
{
Text = allText.Substring(indexOfHighlightString, HighlightedText.Length),
Background = Consts.SearchHighlightColor,
});
Inlines.Add(allText.Substring(indexOfHighlightString + HighlightedText.Length));
}
}
private string GetCompleteText()
{
var allText = Inlines.OfType<Run>().Aggregate(new StringBuilder(), (sb, run) => sb.Append(run.Text), sb => sb.ToString());
return allText;
}
}
Still not sure if there's a better way to do this altogether, but I appear to have found a work around.
I was updating the inlines/runs in a function that was fired by the change notification for the TextProperty and the SearchTextProperty.
Now I'm firing the highlight/update code from a Dispatcher.BeginInvoke() call in the change notification with DispatcherPriority.Normal.
In case anyone wants an example of how to do this, I found this

Resources