Home > Enterprise >  TextBox bound to brush not updating UI despite calling OnPropertyChanged
TextBox bound to brush not updating UI despite calling OnPropertyChanged

Time:11-22

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:

  1. You must give your CanvasExtension control a size greater than 0 to provide an area to draw on. Either configure the Grid rows properly to allow the control to stretch and to prevent the TextBox from covering the CanvasExtension or set CanvasExtension.Width and CanvasExtension.Height explicitly.
  2. Since CanvasExtension is a DependencyObject (it extends Canvas), you should implement properties as DependencyProperty instead of implementing INotifyPropertyChanged. This will improve the performance (and add other benefits).
  3. Because you draw text in UIElement.OnRender, you would have to call UIElement.InvalidateVisual to force rendering i.e. the invocation of UIElement.OnRender on property changes.
    When implementing the CanvasExtension.TextBrush property as DependencyProperty, as suggested in 2), you can configure the property to force the invocation of UIElement.OnRender by setting the FrameworkPropertyMetadataOptions.AffectsRender flag on the property meta data. This is more elegant.
  4. Either set the Binding.UpdateSourceTrigger to UpdateSourceTrigger.Explicit and update the Binding.Source manually (as you did) or simply move the focus away from the TextBox (recommended). The default Binding.Mode of the TextBox.Text property is BindingMode.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>
  • Related