Reputation: 55
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.
Upvotes: 2
Views: 1723
Reputation: 70652
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.
Upvotes: 1