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
CanvasExtensioncontrol a size greater than 0 to provide an area to draw on. Either configure theGridrows properly to allow the control to stretch and to prevent theTextBoxfrom covering theCanvasExtensionor setCanvasExtension.WidthandCanvasExtension.Heightexplicitly. - Since
CanvasExtensionis aDependencyObject(it extendsCanvas), you should implement properties asDependencyPropertyinstead of implementingINotifyPropertyChanged. This will improve the performance (and add other benefits). - Because you draw text in
UIElement.OnRender, you would have to callUIElement.InvalidateVisualto force rendering i.e. the invocation ofUIElement.OnRenderon property changes.
When implementing theCanvasExtension.TextBrushproperty asDependencyProperty, as suggested in 2), you can configure the property to force the invocation ofUIElement.OnRenderby setting theFrameworkPropertyMetadataOptions.AffectsRenderflag on the property meta data. This is more elegant. - Either set the
Binding.UpdateSourceTriggertoUpdateSourceTrigger.Explicitand update theBinding.Sourcemanually (as you did) or simply move the focus away from theTextBox(recommended). The defaultBinding.Modeof theTextBox.Textproperty 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>
