I will use a simplified version of my classes to avoid including too much information. I have this class extending Canvas set as DataContext in Main Window and it writes "Hello world" using Formatted Text with the color of the property textBrush. I have a TextBox and want to be able to change that color through it, however my UI is not updating when the property is?
public class CanvasExtension: INotifyPropertyChanged
{
private SolidColorBrush textBrush
public SolidColorBrush TextBrush
{
get { return textBrush; }
set
{
textBrush= value;
OnPropertyChanged();
}
protected override void OnRender(DrawingContext dc)
{
base.OnRender(dc);
FormattedText formattedTextInput = new FormattedText("Hello world",System.Globalization.CultureInfo.GetCultureInfo("en-US"),
FlowDirection.LeftToRight, new Typeface("Verdana"), 12, TextBrush, 1);
dc.DrawText(formattedTextInput, 0);
}
protected void OnPropertyChanged([CallerMemberName] string name = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
Main Window code:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.DataContext = visualTable;
}
private void TextBox_KeyDown(object sender, KeyEventArgs e)
{
if (e.Key==Key.Enter)
{
TextBox box = (TexBox)sender;
DependencyProperty property = TextBox.TextProperty;
BindingExpression binding = BindingOperations.GetBindingExpression(box,property);
if (binding != null)
{
binding.UpdateSource();
}
KeyBoard.ClearFocus();
}
}
}
And finally, my Xaml:
<Window x:Class="DrawingTutorial.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<local:CanvasExtension x:Name="visualTable"> </local:CanvasExtension>
<TextBox Text="{Binding TextBrush}"></TextBox>
</Grid>
</Window>
What I want to achieve (and I thought should work) is following: I enter some color in the textBox and I press enter. Then, the TextBrush property is updated, and here because I have implemented INotifyPropertyChanged and throw the event in the setter, my UI should update ( be redrawn? I don't know if it is the same in the case of a canvas ) and I should see "Hello World" written with the new color. This however is not happening ( the bindings work, I checked ) until I explicitly call InvalidateVisual(), which I believe beats the purpose of INotifyPropertyChanged. Is there a way to do that without having to manually reset the visual? Note, if I add InvalidateVisual() in the setter after calling OnPropertyChanged(); it updates the UI, the functionality that I need. However, isn't the UI supposed to update exactly because I am calling OnPropertyChanged() and the program "knows" that it has to update?
CodePudding user response:
- You must give your
CanvasExtension
control a size greater than 0 to provide an area to draw on. Either configure theGrid
rows properly to allow the control to stretch and to prevent theTextBox
from covering theCanvasExtension
or setCanvasExtension.Width
andCanvasExtension.Height
explicitly. - Since
CanvasExtension
is aDependencyObject
(it extendsCanvas
), you should implement properties asDependencyProperty
instead of implementingINotifyPropertyChanged
. This will improve the performance (and add other benefits). - Because you draw text in
UIElement.OnRender
, you would have to callUIElement.InvalidateVisual
to force rendering i.e. the invocation ofUIElement.OnRender
on property changes.
When implementing theCanvasExtension.TextBrush
property asDependencyProperty
, as suggested in 2), you can configure the property to force the invocation ofUIElement.OnRender
by setting theFrameworkPropertyMetadataOptions.AffectsRender
flag on the property meta data. This is more elegant. - Either set the
Binding.UpdateSourceTrigger
toUpdateSourceTrigger.Explicit
and update theBinding.Source
manually (as you did) or simply move the focus away from theTextBox
(recommended). The defaultBinding.Mode
of theTextBox.Text
property isBindingMode.LostFocus
. So moving away the focus produces the shortest and simplest code.
CanvasExtension.cs
public class CanvasExtension : Canvas
{
public SolidColorBrush TextBrush
{
get => (SolidColorBrush)GetValue(TextBrushProperty);
set => SetValue(TextBrushProperty, value);
}
public static readonly DependencyProperty TextBrushProperty = DependencyProperty.Register(
"TextBrush",
typeof(SolidColorBrush),
typeof(CanvasExtension),
new FrameworkPropertyMetadata(
default(SolidColorBrush),
FrameworkPropertyMetadataOptions.AffectsRender));
protected override void OnRender(DrawingContext dc)
{
base.OnRender(dc);
FormattedText formattedTextInput = new FormattedText(
"Hello world",
System.Globalization.CultureInfo.GetCultureInfo("en-US"),
FlowDirection.LeftToRight,
new Typeface("Verdana"),
12,
this.TextBrush,
1);
dc.DrawText(formattedTextInput, new Point(0, 0));
}
}
MainWindow.xaml.cs
partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void TextBox_KeyDown(object sender, KeyEventArgs e)
{
switch (e.Key)
{
// Update TextBox.Text binding
case Key.Enter:
(sender as FrameworkElement).MoveFocus(new TraversalRequest(FocusNavigationDirection.Next));
break;
}
}
}
MainWindow.xaml
<Window>
<StackPanel>
<local:CanvasExtension x:Name="visualTable"
Height="200"
Width="200"
HorizontalAlignment="Left"/>
<TextBox PreviewKeyDown="TextBox_KeyDown"
Text="{Binding ElementName=visualTable, Path=TextBrush}" />
</StackPanel>
</Window>