简体   繁体   中英

Input to and Output from User Control in WPF

I made this minimalistic project to learn output and input with user control and it's working as intended. I want to ask, is this a good approach or is there something which is not necessary? I also want to post this, because there is tons of post with specific user cases, but not one with a simple example to learn binding mechanics.

Main Window:

<Window x:Class="OutputFromUserControl.View.OutputFromUserControlWindow"
        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:OutputFromUserControl.View"
        xmlns:uc="clr-namespace:OutputFromUserControl.View.Controls"
        xmlns:vm="clr-namespace:OutputFromUserControl.ViewModel"
        mc:Ignorable="d"
        Title="Output From User Control" Height="450" Width="800">

    <Window.DataContext>
        <vm:MainVM x:Name="MainVM"/>
    </Window.DataContext>

    <StackPanel HorizontalAlignment="Left">
        <Label Content="Form elements:"/>
        <Border CornerRadius="5" BorderBrush="Blue" BorderThickness="1">
            <Grid HorizontalAlignment="Left" >
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="auto"/>
                    <ColumnDefinition Width="auto"/>
                </Grid.ColumnDefinitions>
                <Grid.RowDefinitions>
                    <RowDefinition Height="auto"/>
                    <RowDefinition Height="auto"/>
                    <RowDefinition Height="auto"/>
                </Grid.RowDefinitions>

                <Label Content="Name Input: " Grid.Row="0" Grid.Column="0"/>
                <TextBox Grid.Row="0" Grid.Column="1" 
                     Text="{Binding NameInput, UpdateSourceTrigger=PropertyChanged}"
                     Width="200"
                     />
                <Label Content="Surname Input: " Grid.Row="1" Grid.Column="0"/>
                <TextBox Grid.Row="1" Grid.Column="1" 
                     Text="{Binding SurnameInput, UpdateSourceTrigger=PropertyChanged}"
                     Width="200"
                     />
                <Label Content="Name Output from Control: " Grid.Row="2" Grid.Column="0"/>
                <TextBlock Grid.Row="2" Grid.Column="1" 
                     Text="{Binding FullName}"
                     Width="200"
                     />
            </Grid>
        </Border>
        <Label Content="User Control:" Margin="0,10,0,0"/>
        <Border CornerRadius="5" BorderBrush="Red" BorderThickness="1">
            <uc:NameConcatControl x:Name="NameUC"
                                  NameInput="{Binding NameInput}" 
                                  SurnameInput="{Binding SurnameInput}"
                                  NameOutput="{Binding FullName, Mode=TwoWay}"
                                  />
        </Border>
    </StackPanel>
</Window>

MainVM:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Text;

namespace OutputFromUserControl.ViewModel
{
    public class MainVM : INotifyPropertyChanged
    {
        private string nameInput;

        public string NameInput {
            get { return nameInput; }
            set 
            {
                nameInput = value;
                OnPropertyChanged(nameof(NameInput));
            }
        }

        private string surnameInput;

        public string SurnameInput {
            get { return surnameInput; }
            set {
                surnameInput = value;
                OnPropertyChanged(nameof(SurnameInput));
            }
        }

        private string fullName;

        public string FullName {
            get { return fullName; }
            set {
                fullName = value;
                OnPropertyChanged(nameof(FullName));
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;

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

Control xaml:

<UserControl x:Class="OutputFromUserControl.View.Controls.NameConcatControl"
             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:OutputFromUserControl.View.Controls"
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="auto"/>
            <ColumnDefinition Width="auto"/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="auto"/>
            <RowDefinition Height="auto"/>
            <RowDefinition Height="auto"/>
        </Grid.RowDefinitions>

        <Label Content="Name Input: " Grid.Row="0" Grid.Column="0"/>
        <TextBlock Grid.Row="0" Grid.Column="1" 
                   Text="{Binding NameInput}"
                   x:Name="NameInputTextBlock"
                   />
        <Label Content="Surname Input: " Grid.Row="1" Grid.Column="0"/>
        <TextBlock Grid.Row="1" Grid.Column="1" 
                   Text="{Binding SurnameInput}"
                   x:Name="SurnameInputTextBlock"
                   />
        <Label Content="Name Output: " Grid.Row="2" Grid.Column="0"/>
        <TextBlock Grid.Row="2" Grid.Column="1" 
                   Text="{Binding NameOutput}"
                   x:Name="OutputNameTextBlock"
                   />
    </Grid>
</UserControl>

User control.cs:

using System.Windows;
using System.Windows.Controls;

namespace OutputFromUserControl.View.Controls
{
    /// <summary>
    /// Interaction logic for NameConcatControl.xaml
    /// </summary>
    public partial class NameConcatControl : UserControl
    {
        public string NameInput {
            get { return (string)GetValue(NameInputProperty); }
            set { SetValue(NameInputProperty, value); }
        }

        public static string defaultNameInput = "NameInput";
        public static readonly DependencyProperty NameInputProperty =
            DependencyProperty.Register("NameInput", typeof(string), typeof(NameConcatControl), new PropertyMetadata(defaultNameInput, SetNameOutput));


        public string SurnameInput {
            get { return (string)GetValue(SurnameInputProperty); }
            set { SetValue(SurnameInputProperty, value); }
        }

        public static string defaultSurnameInput = "Surname Input";
        public static readonly DependencyProperty SurnameInputProperty =
            DependencyProperty.Register("SurnameInput", typeof(string), typeof(NameConcatControl), new PropertyMetadata(defaultSurnameInput, SetNameOutput));


        public string NameOutput {
            get { return (string)GetValue(NameOutputProperty); }
            set { SetValue(NameOutputProperty, value); }
        }

        public static string defaultNameOutput = "Name Output";
        public static readonly DependencyProperty NameOutputProperty =
            DependencyProperty.Register("NameOutput", typeof(string), typeof(NameConcatControl), new PropertyMetadata(defaultNameOutput));


        private static void SetNameOutput(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            NameConcatControl control = (NameConcatControl)d;

            string nameInput = "";
            string surnameInput = "";

            if(e.Property.Name == "NameInput")
            {
                string newValue = (string)e.NewValue;
                nameInput = string.IsNullOrEmpty(newValue) ? "" : newValue;
            }
            else
            {
                nameInput = string.IsNullOrEmpty(control.NameInputTextBlock.Text)
                ? ""
                : control.NameInputTextBlock.Text;
            }

            if(e.Property.Name == "SurnameInput")
            {
                string newValue = (string)e.NewValue;
                surnameInput = string.IsNullOrEmpty(newValue) ? "" : newValue;
            }
            else
            {
                surnameInput = string.IsNullOrEmpty(control.SurnameInputTextBlock.Text)
                ? ""
                : control.SurnameInputTextBlock.Text;
            }

            string fullName = $"{nameInput} {surnameInput}";

            control.OutputNameTextBlock.Text = fullName;
            control.NameOutput = fullName;
        }

        public NameConcatControl()
        {
            InitializeComponent();
        }
    }
}

This question has a very wide answers. Different people with different approaches can use for their applications.

But we always follow one common formula. Each view - will have its own view model. (Again in this approach, someone might say might not be true all the time).

From your code (xaml and code), below are my observations.

<Window.DataContext>
    <vm:MainVM x:Name="MainVM"/>
</Window.DataContext>
  1. I generally don't like setting data context in xaml. Instead I prefer to set it on the code-behind (mostly from constructor)

  2. Instead of creating a dependency properties in user control and bind the MainVM properties to the dependency properties of User control.

I prefer to do it this way.

I prefer to create a separate UserControlViewModel.cs and add required properties to it.

public class UserControlViewModel : INotifyPropertyChanged
{
  private string nameInput;

    public string NameInput {
        get { return nameInput; }
        set 
        {
            nameInput = value;
            OnPropertyChanged(nameof(NameInput));
        }
    }

    private string surnameInput;

    public string SurnameInput {
        get { return surnameInput; }
        set {
            surnameInput = value;
            OnPropertyChanged(nameof(SurnameInput));
        }
    }

    private string fullName;

    public string FullName {
        get { return fullName; }
        set {
            fullName = value;
            OnPropertyChanged(nameof(FullName));
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;

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

Then I prefer to add this as a property in MainVM.cs

public class MainVM : INotifyPropertyChanged
{
   private UserControlViewModel _userControlViewModel;

    public UserControlViewModel UserControlViewModel
    {
        get { return _userControlViewModel; }
        set 
        {
            _userControlViewModel = value;
            OnPropertyChanged(nameof(UserControlViewModel));
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;

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

    // Rest of your code
    // You don't need existing properties any more here.
   // If you want to access these properties from MainVM then use the UserControlViewModel property and access the members of it.
}

Then I prefer to set the data-context of my UserControl to this property like below in my MainWindow.xaml

 <uc:NameConcatControl x:Name="NameUC" ="{Binding UserControlViewModel}" />

My usercontrol contorl binding's still remain same as the property names are same and we moved to UserControlViewModel.cs

Now you can remove all dependency properties from code behind of UserControl.xaml.cs

Note:- As I stated at the beginning of my answer, this question has wide area for answers and there are lot of possibilities to answer this question.

I hope I have tried to give you some inputs from my end. I guess this should give you some idea to develop rest..

You can try making those changes and let me know in case if you face any errors or binding issues.

Assuming you just want the full-name view to be something like "Surname, Name", you could actually remove the FullName property from your view model, and just use a MultiBinding (btw the StringFormat property can be used with both MultiBindings and regular Bindings, its pretty nifty if you aren't familiar with it).

As for the Labels, it's good to make a habit of using the simplest control required to get the job done, and in this case, TextBlocks would do just fine, since you don't appear to be using any of the extended functionality the Label offers (ie, BorderBrush, Padding, ContentTemplate, etc.).

You don't generally need to create your own dependency properties in UserControl derived classes, since they are usually designed with a particular viewmodel in mind. They are more useful when the view is independent from the viewmodel, and the dependency properties serve as an api, through which other controls/viewmodels can interact with it.

<Window x:Class="OutputFromUserControl.View.OutputFromUserControlWindow"
    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:OutputFromUserControl.View"
    xmlns:uc="clr-namespace:OutputFromUserControl.View.Controls"
    xmlns:vm="clr-namespace:OutputFromUserControl.ViewModel"
    mc:Ignorable="d"
    Title="Output From User Control" Height="450" Width="800">

<Window.DataContext>
    <vm:MainVM x:Name="MainVM"/>
</Window.DataContext>

<StackPanel HorizontalAlignment="Left">
    <Label Content="Form elements:"/>
    <Border CornerRadius="5" BorderBrush="Blue" BorderThickness="1">
        <Grid HorizontalAlignment="Left" >
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="auto"/>
                <ColumnDefinition Width="auto"/>
            </Grid.ColumnDefinitions>
            <Grid.RowDefinitions>
                <RowDefinition Height="auto"/>
                <RowDefinition Height="auto"/>
                <RowDefinition Height="auto"/>
            </Grid.RowDefinitions>

            <TextBlock Text="Name Input:" Grid.Row="0" Grid.Column="0"/>
            <TextBox Grid.Row="0" Grid.Column="1" 
                 Text="{Binding NameInput, UpdateSourceTrigger=PropertyChanged}"
                 Width="200"
                 />
            <TextBlock Text="Surname Input:" Grid.Row="1" Grid.Column="0"/>
            <TextBox Grid.Row="1" Grid.Column="1" 
                 Text="{Binding SurnameInput, UpdateSourceTrigger=PropertyChanged}"
                 Width="200"
                 />
            <TextBlock Text="Name Output from Control:" Grid.Row="2" Grid.Column="0"/>
            <TextBlock Grid.Row="2" Grid.Column="1" Width="200">
                <MultiBinding StringFormat="{}{0}, {1}">
                    <Binding Path="SurnameInput"/>
                    <Binding Path="NameInput"/>
                </MultiBinding>
            </TextBlock>
        </Grid>
    </Border>
    <Label Content="User Control:" Margin="0,10,0,0"/>
    <Border CornerRadius="5" BorderBrush="Red" BorderThickness="1">
        <uc:NameConcatControl x:Name="NameUC"
                              NameInput="{Binding NameInput}" 
                              SurnameInput="{Binding SurnameInput}"
                              NameOutput="{Binding FullName}"
                              />
    </Border>
</StackPanel>

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