简体   繁体   中英

How to set a parameter for a user control that has a view model in WPF

In WPF I have a window that includes a user control. The window and user control each have a view model. I want to pass a parameter from the window's VM to the UC's VM. After a fair amount of looking, I haven't found a way.

The window XAML sets its data context to its VM. The UC includes a custom dependency property for the parameter. I want to use SetBinding to bind the DP to the UC VM.

If I set the UC data context to its VM, then the parameter binding doesn't work. If I don't set the UC data context then the parameter binding works but the UC VM is not referenced.

How can I pass a parameter AND bind to the UC VM?

UC XAML

<UserControl x:Name="userControl" x:Class="Test_Paramaterized_UserControl_with_MVVM.UserControl1"
             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" 
             xmlns:local="clr-namespace:Test_Paramaterized_UserControl_with_MVVM"
             xmlns:view="clr-namespace:Daavlin.SmartTouch.STUV_WPF.View"
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300">
      <Grid Margin="10">
        <Border BorderThickness="3" BorderBrush="Black" Padding="10">
            <StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
                <StackPanel Orientation="Horizontal">
                    <TextBlock Text="UserControl1 View: "/>
                    <TextBlock Text="{Binding ElementName=userControl, Path=PropUserControlView, Mode=OneWay}" FontWeight="Bold"/>
                </StackPanel>
                <Rectangle Height="5"/>
                <StackPanel Orientation="Horizontal">
                    <TextBlock Text="UserControl1 ViewModel: " />
                    <TextBlock Text="{Binding PropUserControlViewModel, FallbackValue=propUserControlViewModel 2}" FontWeight="Bold">
                        <TextBlock.DataContext>
                            <local:UserControl1ViewModel/>
                        </TextBlock.DataContext>
                    </TextBlock>
                </StackPanel>
            </StackPanel>
        </Border>
    </Grid>
</UserControl>

UC code-behind & VM

public partial class UserControl1 : UserControl
    {
        public UserControl1()
        {
            InitializeComponent();
        }

        public string PropUserControlView { get => (string)GetValue(PropUserControlViewProperty); set => SetValue(PropUserControlViewProperty, value); }
        public static readonly DependencyProperty PropUserControlViewProperty =
            DependencyProperty.Register(nameof(PropUserControlView), typeof(string), typeof(UserControl1),
                new PropertyMetadata(null, DependencyPropertyChanged));

        private static void DependencyPropertyChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs)
        {
            var x = dependencyPropertyChangedEventArgs.NewValue;
        }
    }

    public class UserControl1ViewModel : ObservableObject
    {
        public string PropUserControlViewModel { get => _propUserControlViewModel; set => SetField(ref _propUserControlViewModel, value); }
        private string _propUserControlViewModel = "value from UserControl-ViewModel";
    }

Window XAML

<Window x:Class="Test_Paramaterized_UserControl_with_MVVM.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:Test_Paramaterized_UserControl_with_MVVM"
        Title="MainWindow" >
    <Window.DataContext>
        <local:MainWindowViewModel />
    </Window.DataContext>
    <Grid VerticalAlignment="Top" >
        <StackPanel VerticalAlignment="Center" HorizontalAlignment="Center" Margin="20">
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="MainWindow1 ViewModel: "/>
                <TextBox Text="{Binding PropWindowViewModel, UpdateSourceTrigger=PropertyChanged}" FontWeight="Bold"/>
            </StackPanel>
            <Rectangle Height="10"/>
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="UserControl1 (fixed value Fixed): " VerticalAlignment="Center"/>
                <local:UserControl1 PropUserControlView="Fixed"/>
            </StackPanel>
            <Rectangle Height="10"/>
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="UserControl1 (bound to MainWindows VM): " VerticalAlignment="Center"/>
                <local:UserControl1 PropUserControlView="{Binding PropWindowViewModel}"/>
            </StackPanel>
        </StackPanel>
    </Grid>
</Window>

Window code-behind & VM

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

public class MainWindowViewModel : ObservableObject
{
    public string PropWindowViewModel { get => _propWindowViewModel; set => SetField(ref _propWindowViewModel, value); }
    private string _propWindowViewModel = "valuefrom Window-VIewModel";
}

As far as I understood, what you meant was :-

1) You have a user control which has its own view model.

2) You have a Window where you have its own view model.

You want to link both and pass parameters from your WindowViewModel to UserControlViewModel.

What you can do is, Keep a property (eg UCViewModel ) of type UserControlViewModel in your WindowViewModel and set the datacontext of the user control in your XAML to

<local:UserControl1 DataContext="{Binding UCViewModel}" .../>

Now that you can access anything that is there in your UserControlViewModel via WindowViewModel, you can set any property value OR pass any parameter to your UserControlViewModel from WindowViewModel.

If you need a code reference, let me know. We have been using user controls in a similar way and it works fine.

I want to use SetBinding to bind the DP to the UC VM.

Is that really a requirement? SetBinding() requires that the target property be a dependency property, which in turn requires that the target object be a dependency object. Your view model object is not a dependency object, and of course none of its properties are dependency properties.

Achieving that goal would require a much bigger change to your code than is otherwise apparently necessary.

If I set the UC data context to its VM, then the parameter binding doesn't work

Why not? You didn't show code that attempts this, so it's difficult to understand what you mean here. It's not a good idea to have the user control set its own DataContext anyway. That property is public, and you don't want to expose your implementation details to client code. Doing so invites bugs where the client code has set the DataContext to the wrong thing, disabling everything in your UserControl .

But that said, if by "parameter binding" you mean the binding in the MainWindow XAML, assigning {Binding PropWindowViewModel} to the PropUserControlView property of the user control, then just setting the DataContext of the user control should not affect that. You still have the dependency property in the user control, and anything bound that within the user control should still work.

Finally, it's not entirely clear why you want the dependency property tied to the view model. In the user control's XAML, you can (as you've already done) bind directly to the user control's dependency property. There's no need for a property in the view model to replicate that.

Maybe you have code in the view model somewhere else that wants to respond to changes in this value? It's not clear, and it's difficult to give the best advice without knowing the whole story.

All that said, the code you posted above can be made to work with a couple of small changes. First, you'll need to expose the TextBlock where you've created the view model, so that the user control code-behind has access to it:

<TextBlock x:Name="textBlock1" Text="{Binding PropUserControlViewModel, FallbackValue=propUserControlViewModel 2}" FontWeight="Bold">
  <TextBlock.DataContext>
    <l:UserControl1ViewModel/>
  </TextBlock.DataContext>
</TextBlock>

Ie add the x:Name="textBlock1" to the declaration.

Then, you need to use the property-change notification for your dependency property to update the view model property any time the dependency property changes:

private static void DependencyPropertyChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs)
{
    UserControl1 uc = (UserControl1)dependencyObject;
    UserControl1ViewModel vm = (UserControl1ViewModel)uc.textBlock1.DataContext;
    vm.PropUserControlViewModel = (string)dependencyPropertyChangedEventArgs.NewValue;
}

The above works in your limited example, but you'll probably want to give the DependencyPropertyChanged() method a more descriptive name, specific to the actual property in question.

If you do choose to mirror the dependency property in the view model this way, IMHO a better way to do that would be to set the user control's root element (ie the Grid ) so that its data context is your view model, and then throughout the rest of the XAML, bind only to the view model. Mixing the view model and dependency property is not wrong per se, but it does introduce an inconsistency that can make it harder to test and maintain the code.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM