Search code examples
c#wpfxaml2-way-object-databinding

Binding Height property of a TextBox


I need to bind the height of TextBoxes to my data model. The model should be updated each time a TextBox gets resized. The TextBox is resized because it wraps the content until MaxHeight is reached, when it's reached it shows the scrollbar. I've made a little example to demonstrate my problem.

<Window x:Class="BindingTester.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:local="clr-namespace:BindingTester"
    mc:Ignorable="d"
    Title="MainWindow" Height="350" Width="525">
<Canvas>
    <TextBox 
        Canvas.Left="234" 
        Canvas.Top="71"
        Text="TextBox"
        TextWrapping="Wrap"
        ScrollViewer.VerticalScrollBarVisibility="Auto" 
        MaxHeight="200"
        AcceptsReturn="True"
        Height="{Binding TextBoxHeight, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged/>
</Canvas>

public partial class MainWindow : Window, INotifyPropertyChanged
{
    private double textBoxHeight;

    public MainWindow()
    {
        InitializeComponent();
        DataContext = this;
        //TextBoxHeight = 100.0;
    }

    public double TextBoxHeight
    {
        get { return textBoxHeight; }
        set
        {
            if (value != textBoxHeight)
            {
                textBoxHeight = value;
                RaisePropertyChanged("TextBoxHeight");
            }
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;

    private void RaisePropertyChanged(string propertyName)
    {
        PropertyChangedEventHandler handler = PropertyChanged;
        if (handler != null)
        {
            handler(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

When I use this, the binding from source to target works fine. When I set the TextBoxHeight property in the constructor of the MainWindow the TextBox resizes perfectly to 100, but the size seems to be fixed then. When I don't set it (because with a height of 100 it's much to large for the content "TextBox") first behaves like expected: the TextBox's size fits the content. The TextBoxHeight in the model gets updated to NaN.

I know this happens because the Height property returns NaN when it is not set, instead I have to ask for ActualHeight. But anyway, I recognized that if I enter some text (for example newlines) to resize the TextBox's Height, the TextBoxHeight property still is not updated and the setter is not called again…I also have tried to use an IValueConverter to update it using ActualHeight without success.

I know in this example, even if I resize the TextBox by entering newlines, TextBoxHeight each time would be updated with NaN, but the setter is just called the first time when the TextBox is initialized. It confuses me that the Binding doesn't seem to work…I know a solution for the problem itself: subscribe the SizeChanged event, get the DataContext of sender object and set the model manually. But I think there should be a solution without subscribing and accessing the model in code-behind, only binding the properties. Can anybody help?


Solution

  • I can suggest you to use behavior or some another event to command mechanism and then you can listen to the OnResizeAction event to handle the size changes and convert these event to any logic you want. Here is a small example. 1. Xaml code:

    <Window x:Class="SoResizeIssue.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:soResizeIssue="clr-namespace:SoResizeIssue"
        xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
        Title="MainWindow" Height="350" Width="525">
    <Window.DataContext>
        <soResizeIssue:MainViewModel/>
    </Window.DataContext>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"></RowDefinition>
            <RowDefinition Height="*"></RowDefinition>
        </Grid.RowDefinitions>
        <TextBox HorizontalAlignment="Stretch" VerticalAlignment="Stretch" 
                 Text="{Binding ContentFromDataContext, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}">
            <i:Interaction.Behaviors>
                <soResizeIssue:ResizeHandlingBehavior OnResizeAction="{Binding OnResizeAction, UpdateSourceTrigger=PropertyChanged}"/>
            </i:Interaction.Behaviors>
        </TextBox>
    </Grid></Window>
    

    2. Behavior code:

    public class ResizeHandlingBehavior:Behavior<FrameworkElement>
    {
        protected override void OnAttached()
        {
            base.OnAttached();
            AssociatedObject.SizeChanged += AssociatedObjectOnSizeChanged;
        }
    
        protected override void OnDetaching()
        {
            base.OnDetaching();
            AssociatedObject.SizeChanged -= AssociatedObjectOnSizeChanged;
        }
    
        private void AssociatedObjectOnSizeChanged(object sender, SizeChangedEventArgs args)
        {
            var action = OnResizeAction;
            if(action == null) return;
            action(args);
        }
    
        public static readonly DependencyProperty OnResizeActionProperty = DependencyProperty.Register(
            "OnResizeAction", typeof (Action<object>), typeof (ResizeHandlingBehavior), new PropertyMetadata(default(Action<object>)));
    
        public Action<object> OnResizeAction
        {
            get { return (Action<object>) GetValue(OnResizeActionProperty); }
            set { SetValue(OnResizeActionProperty, value); }
        }
    }
    

    3. ViewModel code:

    public class MainViewModel:BaseObservableObject
    {
        private string _contentFromDataContext;
        private Action<object> _onResizeAction;
    
        public MainViewModel()
        {
            OnResizeAction = new Action<object>(InnerOnResizeAction);
        }
    
        private void InnerOnResizeAction(object obj)
        {
            var args = obj as SizeChangedEventArgs;
            //do you logic here
        }
    
        public string ContentFromDataContext
        {
            get { return _contentFromDataContext; }
            set
            {
                _contentFromDataContext = value;
                OnPropertyChanged();
            }
        }
    
        public Action<object> OnResizeAction
        {
            get { return _onResizeAction; }
            set
            {
                _onResizeAction = value;
                OnPropertyChanged();
            }
        }
    } 
    

    Updates 5. This solution moves the logic of Resize to the ViewModel side, while the TextBlock will change his size due to the Layout re-build and actual size changes. Using this solution you don't do the binding to some actual size parameters you just observe the size changes and move all the logic from window code behind to the view model side, thus the window's (xaml.cs) code behind stays completely clear.

    1. There are another way to redirect an event trigger to view model named Trigger to Command pattern, here are the links: Binding WPF Events to MVVM ViewModel Commands, How to trigger ViewModel command for a specific button events and Firing a Command within EventTrigger of a style?.

    I hope it will help you. Thanks and regards,