2

I'm learning wpf and I'm trying to change a label value based on the number of characters in my RichTextBox I thought I could do this inside of my richTextBox_TextChanged() method where the current logic is to remove all text entered passed 140 characters.

private void richTextBox_TextChanged(object sender, TextChangedEventArgs e)
    {
        TextRange range = new TextRange(richTextBox.Document.ContentStart, richTextBox.Document.ContentEnd);
        var text = range.Text.Trim();
        label.Content = text.Length;
        if (text.Length > 140)
        {
            int split = 0;
            while (richTextBox.CaretPosition.DeleteTextInRun(-1) == 0)
            {
                richTextBox.CaretPosition.GetPositionAtOffset(--split);

            }
        }
    }

This crashes at runtime because of this line label.Content = text.Length; where I was using this line to see if I could get a length of zero at start to see if it would work at all. The error was: "An exception of type 'System.NullReferenceException' occurred in ApplicationName.exe but was not handled in user code".

The logic for only allowing only 140 characters works fine and the name 'label' is also the name of my label UI element.

What would I need to do to change my label's value to length of the text field and have it change as the user types.

  • *"This wouldn't compile"* Don't believe you. I see no evidence in your question that the compiler didn't complete successfully. I suggest you [edit], add the compilation error you get, and prove me a liar. –  Mar 23 '17 at 18:25
  • On it, sorry about that, I just added it to the question – LearningWPFrn Mar 23 '17 at 18:26
  • so this actually does compile but crashes at runtime? – bashis Mar 23 '17 at 18:30
  • That makes way more sense than what I said, I will update my description – LearningWPFrn Mar 23 '17 at 18:31
  • 2
    If you're trying to learn WPF, learn MVVM. With WPF, time spent doing things in code behind just delays learning anything useful about the framework. – 15ee8f99-57ff-4f92-890c-b56153 Mar 23 '17 at 18:38
  • @EdPlunkett that is my ultimate goal, I figured I should learn the basics of WPF first. Does the MVVM framework do things completely differently? – LearningWPFrn Mar 23 '17 at 18:42
  • @LearningWPFrn Differently, yeah. But then again RichTextBox is weird enough that on reflection maybe you are actually better off doing that particular bit in code behind. Your current issue btw looks like either `text` is null, or `label` is null. I'd put a breakpoint at the start of the event handler you shared, hover the mouse over everything on each line before you F10 onto it, and see which identifier is `null`. Shouldn't be a tough one at all. Where is `label` coming from? – 15ee8f99-57ff-4f92-890c-b56153 Mar 23 '17 at 19:00
  • 1
    @EdPlunkett the label was null... That fixed it. I will post an answer shortly unless you want to. Also, I think its a good idea to go ahead and jump into MVVM because a lot of my other solutions for this little example project I gave myself don't translate well to the architecture. – LearningWPFrn Mar 23 '17 at 19:15
  • 1
    @LearningWPFrn Proper MVVM requires a very different way of thinking about how applications work. The more you delay doing it "The WPF Way", the longer it will take you to unlearn the bad habits that doing things in code-behind taught you. – Bradley Uffner Mar 23 '17 at 19:23
  • Don't be fanatical about avoiding code behind (it's a last resort but it's there for a reason) there for a reason), but once I got comfortable doing things "the WPF way" I found that it's a really powerful way to write ui. But it's a steep learning curve. – 15ee8f99-57ff-4f92-890c-b56153 Mar 23 '17 at 19:51

1 Answers1

1

Without a good Minimal, Complete, and Verifiable code example, it's impossible to know for sure what the issue is. You can find a lot of useful advice regarding how to debug, diagnose, and fix NullReferenceException here: What is a NullReferenceException, and how do I fix it?

That said, I think it's likely that the issue is caused by the TextChanged event being raised before your label field has been initialized, probably as part of the InitializeComponent() method execution. It's just that things are in the wrong order.

You could address the problem simply by checking the label field for null before trying to use it. But a) this adds to the complexity of the code, and b) may leave your Label control uninitialized until you've explicitly set it, or the text is changed later.

A better way would be, in fact, to embrace the normal WPF paradigm of keeping a view model and binding to it. As Ed notes in his comment, because of how RichTextBox maintains its contents, and in particular because there is not a convenient, simple string-only property you can track, you may still want the code-behind to handle the TextChanged event. But in that event, you can still access a proper view model and let that do the work.

Doing this, WPF will make sure both that it doesn't try to dereference a null value, and that if and when the Label control is finally initialized, it's correctly initialized to the value you expect.

Here's a simple view model that has a single property for this purpose, and which contains boilerplate logic that would be typical for any view model class (you can find these examples all over Stack Overflow…I'm just providing this here for convenience and consistency with the rest of this post):

class ViewModel : INotifyPropertyChanged
{
    private int _textLength;

    public int TextLength
    {
        get { return _textLength; }
        set { _UpdateField(ref _textLength, value); }
    }

    public event PropertyChangedEventHandler PropertyChanged;

    private void _UpdateField<T>(ref T field, T newValue, [CallerMemberName] string propertyName = null)
    {
        if (!EqualityComparer<T>.Default.Equals(field, newValue))
        {
            field = newValue;
            _OnPropertyChanged(propertyName);
        }
    }

    private void _OnPropertyChanged(string propertyName)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));

        switch (propertyName)
        {
            // you can add "case nameof(...):" cases here to handle
            // specific property changes, rather than polluting the
            // property setters themselves
        }
    }
}

With a view model like that, then you can write your XAML:

<Window x:Class="TestSO42984032TextLengthLabel.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"
        xmlns:l="clr-namespace:TestSO42984032TextLengthLabel"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">
  <Window.DataContext>
    <l:ViewModel/>
  </Window.DataContext>
  <StackPanel>
    <RichTextBox TextChanged="RichTextBox_TextChanged"/>
    <Label Content="{Binding TextLength}"/>
  </StackPanel>
</Window>

And of course, you'll need the code-behind:

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }

    private void RichTextBox_TextChanged(object sender, TextChangedEventArgs e)
    {
        ViewModel model = (ViewModel)DataContext;
        RichTextBox richTextBox = (RichTextBox)sender;
        TextRange range = new TextRange(richTextBox.Document.ContentStart, richTextBox.Document.ContentEnd);
        var text = range.Text.Trim();

        model.TextLength = text.Length;
        if (text.Length > 140)
        {
            int split = 0;
            while (richTextBox.CaretPosition.DeleteTextInRun(-1) == 0)
            {
                richTextBox.CaretPosition.GetPositionAtOffset(--split);

            }
        }
    }
}

Now, any time the text changes, your event handler will be called, and as part of what it does, it will update the view model property with the correct value. The DataContext will be sure to be set at this point, so you can safely use it without concern for a null reference.

If for some reason it would be useful to have the plain text information as well, you can extend your view model to include that:

class ViewModel : INotifyPropertyChanged
{
    private string _text;
    private int _textLength;

    public string Text
    {
        get { return _text; }
        set { _UpdateField(ref _text, value); }
    }

    public int TextLength
    {
        get { return _textLength; }
        set { _UpdateField(ref _textLength, value); }
    }

    public event PropertyChangedEventHandler PropertyChanged;

    private void _UpdateField<T>(ref T field, T newValue, [CallerMemberName] string propertyName = null)
    {
        if (!EqualityComparer<T>.Default.Equals(field, newValue))
        {
            field = newValue;
            _OnPropertyChanged(propertyName);
        }
    }

    private void _OnPropertyChanged(string propertyName)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));

        switch (propertyName)
        {
            case nameof(Text):
                TextLength = Text.Length;
                break;
        }
    }
}

Note that here, I'm using the switch statement to update the TextLength property. Your code-behind would look like this instead:

private void RichTextBox_TextChanged(object sender, TextChangedEventArgs e)
{
    ViewModel model = (ViewModel)DataContext;
    RichTextBox richTextBox = (RichTextBox)sender;
    TextRange range = new TextRange(richTextBox.Document.ContentStart, richTextBox.Document.ContentEnd);
    var text = range.Text.Trim();

    model.Text = text;
    if (text.Length > 140)
    {
        int split = 0;
        while (richTextBox.CaretPosition.DeleteTextInRun(-1) == 0)
        {
            richTextBox.CaretPosition.GetPositionAtOffset(--split);

        }
    }
}

Finally, note that bindings can use property paths, not just simple property names. So if you want, you can omit the TextLength property altogether:

class ViewModel : INotifyPropertyChanged
{
    private string _text = string.Empty;

    public string Text
    {
        get { return _text; }
        set { _UpdateField(ref _text, value); }
    }

    public event PropertyChangedEventHandler PropertyChanged;

    private void _UpdateField<T>(ref T field, T newValue, [CallerMemberName] string propertyName = null)
    {
        if (!EqualityComparer<T>.Default.Equals(field, newValue))
        {
            field = newValue;
            _OnPropertyChanged(propertyName);
        }
    }

    private void _OnPropertyChanged(string propertyName)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));

        switch (propertyName)
        {
            // empty
        }
    }
}

and change the XAML to this:

<Window x:Class="TestSO42984032TextLengthLabel.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"
        xmlns:l="clr-namespace:TestSO42984032TextLengthLabel"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">
  <Window.DataContext>
    <l:ViewModel/>
  </Window.DataContext>
  <StackPanel>
    <RichTextBox TextChanged="RichTextBox_TextChanged"/>
    <Label Content="{Binding Text.Length}"/>
  </StackPanel>
</Window>

Note that in this case, you need to initialize your view model field, to ensure it has an actual non-null string value. Without that change, the program will run, but your Label will initially have no value set.

Hope that helps. As you can see, even within the view-model paradigm, there are lots of variations, depending on what's important in the rest of the program. But this should get you pointed in the right direction.

Community
  • 1
  • 1
Peter Duniho
  • 68,759
  • 7
  • 102
  • 136