1

I've this simplified case:

  • MainWindow shows a TextBox and a UserControl.
  • The UserControl has a dependency property to receive the TextBox text (bound in window XAML)
  • The received text is bound to a view-model property in UserControl code-behind.
  • The view-model converts the input text into camel case.
  • Camel case text is displayed using a TextBlock in UserControl.

I can see the user control receives the correct value of the TextBox, however this value doesn't reach the view-model, I can't understand why.

MainWindow

Window x:Class="WpfApp5.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:WpfApp5"
    mc:Ignorable="d"
    Title="MainWindow" Height="450" Width="800">

    <StackPanel>
        <Label Content="Original:"/>
        <TextBox x:Name="tb"/>
        <local:CamelStringBox OriginalText="{Binding ElementName=tb, Path=Text, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"/>
    </StackPanel>
</Window>

Code-behind:

using System.Windows;

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

UserControl CamelStringBox

<UserControl xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
         xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
         xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
         mc:Ignorable="d" 
         
         x:Class="WpfApp5.CamelStringBox"
         xmlns:local="clr-namespace:WpfApp5">

    <UserControl.DataContext>
        <local:CamelStringBoxViewModel x:Name="vm"/>
    </UserControl.DataContext>

    <StackPanel>
        <Label Content="Camelized String:"/>
        <TextBlock Text="{Binding ElementName=vm, Path=CamelizedText}"/>
    </StackPanel>
</UserControl>

Code-behind:

using System.ComponentModel;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;

namespace WpfApp5 {

    public partial class CamelStringBox : UserControl {

        public string OriginalText {
            get { return (string) GetValue (OriginalTextProperty); }
            set { SetValue (OriginalTextProperty, value); }
        }

        public static readonly DependencyProperty OriginalTextProperty =
            DependencyProperty.Register (
                "OriginalText", typeof (string), 
                typeof (CamelStringBox), 
                new PropertyMetadata (
                    (d,e) => Debug.WriteLine ($"Value received by UserControl: {e.NewValue}")));

        public CamelStringBox () {
            InitializeComponent ();

            // Bind user control OriginalText to view-model Text (doesn't work)
            if (DataContext == null) Debug.WriteLine ($"{GetType ()}: DataContext is null");
            else SetBinding (OriginalTextProperty, new Binding {
                Path = new PropertyPath (nameof (CamelStringBoxViewModel.Text)),
                Mode = BindingMode.TwoWay
            });
        }
    }

    public class CamelStringBoxViewModel : ChangeNotifier {
        private string text;
        private string camelizedText;

        public string Text {
            get => text;
            set {
                text = value;
                RaisePropertyChanged ();
            }
        }

        public string CamelizedText {
            get => camelizedText;
            private set {
                camelizedText = value;
                RaisePropertyChanged ();
            }
        }

        // Never triggered
        private void OnPropertyChanged (object sender, PropertyChangedEventArgs e) {
            if (e.PropertyName == nameof (Text)) CamelizedText = Camelize (Text);
        }

        private string Camelize (string text) {
            if (text == null || text.Length == 0) return null;
            string[] s = text.ToLower ().Split (' ');
            for (int i = 0; i < s.Length; i++) s[i] = char.ToUpper (s[i][0]) + s[i].Substring (1);
            return string.Join (" ", s);
        }
    }

    public class ChangeNotifier : INotifyPropertyChanged {
        public event PropertyChangedEventHandler PropertyChanged;
        public void RaisePropertyChanged ([CallerMemberName] string propertyName = null) {
            PropertyChanged?.Invoke (this, new PropertyChangedEventArgs (propertyName));
        }
    }
}
mins
  • 6,478
  • 12
  • 56
  • 75
  • Don't set `DataContext` in the user control. It seems you want to have custom control with own dependency properties, then it won't ever need view model. Just add properties directly to control and access them in [TemplateBinding](https://learn.microsoft.com/en-us/dotnet/framework/wpf/advanced/templatebinding-markup-extension) way. – Sinatr Aug 24 '20 at 15:24
  • @Sinatr: Thanks, "*It seems you want to have custom control with own dependency properties*", I want to use a clear MVVM pattern, hence the view-model, but it seems I need a dependency property to get the input string from the main window. – mins Aug 24 '20 at 15:27
  • MVVM won't work with custom controls, same as you can't replace behaviors with MVVM. The custom control itself can be used as a view for viewmodel, but that would be some special viewmodel, not the one which you are trying to create now. – Sinatr Aug 24 '20 at 15:27
  • A UserControl must not have its own, private view model object, i.e. it must not explicitly set its own DataContext. The typical, DataContext-based Binding of its properties won't work then. It should instead inherit the DataContext from its parent element (e.g. a Window) and its properties should be bound to that DataContext, like `` – Clemens Aug 24 '20 at 15:36
  • @Clemens, thanks. In my actual case (which I tried to simplify into this example) I want to create a user control combining a ListBox, a ComboBox and two buttons (previous/next) to create a selection list with its history (view and logic). To be reusable, I guess it must have two properties (input items list, and currently selected item). Can I use your approach in this case? How will the user of the control get those two properties if only the window data context is used? – mins Aug 24 '20 at 15:50
  • Any dependency property that is exposed by the UserControl (e.g. an IEnumerable Items property) can be bound to a view model (e.g. ``) and internally be used by the elements in the UserControls XAML by a RelativeSource Binding like `` – Clemens Aug 24 '20 at 15:57
  • If a specialized view model is supposed the be used by the UserControl, it might be exposed as a property of the main view model, and you would bind to it like `` – Clemens Aug 24 '20 at 15:59
  • @Clemens this is a helpful confirmation. Can you please tell me whether rthe problem with my current code is in the piece `SetBinding (OriginalTextProperty, new Binding...` which can't work in a user control? – mins Aug 24 '20 at 16:03
  • Your view model doesn't do what you think. There is no connection between the Text and CamelizedText properties. Firing the PropertyChanged event doesn't magically call the OnPropertyChanged method. You may however simply call Camelize in the Text setter. – Clemens Aug 24 '20 at 16:26
  • @Clemens: Ah I see I lost the INPC event registration in the simplification. I've now changed my code to remove the view-model and have no backup, so can't be sure, but I think when the callback was registered correctly it was never called actually. Anyway, thanks again, I found good elements on suggested duplicate questions. – mins Aug 24 '20 at 16:42

0 Answers0