Search code examples
wpfdynamicdata-bindinguser-controls

WPF, how to destroy and recreate usercontrol based on changes in other, & binding problem


I am new to WPF and C#,

I have a main screen consist of multiple user controls and some of the need to be destroyed and recreate on the fly based on the selection of one.

Here is my sample WMMW code,

Models

public class Method : INotifyPropertyChanged, IDataErrorInfo
    {
        #region properties

        private string _method;
        private string _helper;

        #endregion

        public Method()
        {
            _method = "MM1";
            _helper = "HM1";
        }
//getter setters..
}

public class Property : INotifyPropertyChanged
    {
        #region Properties

        private string _name;
        private string _path;
        private float _standarddeviation;
        private string _unit;

//getter setters
}

MethodViewModel

    class MethodViewModel
{
    #region Properties

    private Method _method;

    #endregion



    #region Getter & Setters

    public Method Method
    {
        get { return _method; }

    }

    public ICommand UpdateCommand
    {
        get; private set;
    }
    #endregion

    #region Constructor
    /// <summary>
    /// Initialize a new interface of the MEthodViewModel class
    /// </summary>
    public MethodViewModel()
    {
        //test
        _method = new Method();
        UpdateCommand = new MethodUpdateCommand(this);
    }
    #endregion


    #region Functions



    public void SaveChanges()
    {

        //TODO: Destroy and rebuild the usercontrol

    }

    #endregion


}

Command

    class MethodUpdateCommand : ICommand
{
    private MethodViewModel _viewModel;


    /// <summary>
    /// Initialize a new instance of MethodNameUpdate Command
    /// </summary>
    /// <param name="viewModel"></param>
    public MethodUpdateCommand(MethodViewModel viewModel)
    {
        _viewModel = viewModel;
    }



    #region ICOmmand Members

    public event EventHandler CanExecuteChanged
    {
        add { CommandManager.RequerySuggested += value; }
        remove { CommandManager.RequerySuggested -= value; }
    }


    public bool CanExecute(object parameter)
    {
        return String.IsNullOrWhiteSpace(_viewModel.Method.Error);
    }

    public void Execute(object parameter)
    {
        _viewModel.SaveChanges();
    }

    #endregion
}

Main screen

<Window x:Class="WpfApplicationTest.Views.MainScreen"
    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:WpfApplicationTest.Views"
    xmlns:control="clr-namespace:WpfApplicationTest.Controls"

    mc:Ignorable="d"
    Title="MainScreen" Height="573.763" Width="354.839">
<Grid Margin="0,0,0,-41">
    <control:MethodControl Margin="21,23,63,460" RenderTransformOrigin="0.507,0.567"></control:MethodControl>

    <control:PropertyControl Margin="0,129,0,-129"></control:PropertyControl>

</Grid>

Method Control

<UserControl x:Class="WpfApplicationTest.Controls.MethodControl"
         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:WpfApplicationTest.Controls"
         xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
         mc:Ignorable="d" d:DesignWidth="300" Height="101.075">

<WrapPanel  Orientation=" Horizontal" VerticalAlignment="Top" Height="120" >
    <Label Content="Method Name:" Width="113"/>
    <ComboBox Width="160" SelectedItem="{Binding Method.Name, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}" ItemsSource="{StaticResource MethodNames}" >
        <i:Interaction.Triggers>
            <i:EventTrigger EventName="SelectionChanged">
                <i:InvokeCommandAction Command="{Binding UpdateCommand}"/>
            </i:EventTrigger>

        </i:Interaction.Triggers>

    </ComboBox>
    <Label Content="Reflection Type:" Width="113"/>
    <ComboBox Width="160" SelectedItem="{Binding Method.Helper, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}" ItemsSource="{StaticResource HelperMethods}">
        <i:Interaction.Triggers>
            <i:EventTrigger EventName="SelectionChanged">
                <i:InvokeCommandAction Command="{Binding UpdateCommand}"/>
            </i:EventTrigger>

        </i:Interaction.Triggers>
    </ComboBox>


</WrapPanel>

Property control.xaml

    <StackPanel Name="Test"></StackPanel>

    public partial class PropertyControl : UserControl
{
    public PropertyControl()
    {
        InitializeComponent();
        PopulatePropertyPanel("MM1", "HM1");
    }

    private void PopulatePropertyPanel(string name, string reflection)
    {
        //TODO: decide which mthod
        //int methodindex = Constant.GetMethodNameIndex(name);
        int methodindex = Array.IndexOf((String[])Application.Current.Resources["MethodNames"], name);


        switch (methodindex)
        {
            case 0:

                foreach (String prop in (String[])Application.Current.Resources["Result1"])
                {

                    PopulateProperty(prop, true);


                }
                break;

            default:

                foreach (String prop in (String[])Application.Current.Resources["Result2"])
                {

                    PopulateProperty(prop, false);
                }
                break;
        }


    }

    private void PopulateProperty(string prop, Boolean constant)
    {


        Label lbl = new Label();
        lbl.Content = prop;
        TextBox pathtext = new TextBox();
        pathtext.Text = "path";
        TextBox std = new TextBox();
        std.Text = "std";
        TextBox unit = new TextBox();
        unit.Text = "unit";

        Test.Children.Add(lbl);
        Test.Children.Add(pathtext);
        Test.Children.Add(std);
        Test.Children.Add(unit);


    }
}

I want to recreate populate property-control, every time there is a change in method-control, which I already create a command for it.

Also, how can I bind the components in property control with property model, I need to have a collection of properties (1 property for each result, and destroy and rebuild the collection with property-control.

EDIT 1:

Main window

    <ContentControl Grid.Row="1" Content="{Binding ChildViewModel}" />

Resources

 <DataTemplate DataType="{x:Type modelViews:PropertyViewModel}">
    <control:PropertyControl  />
</DataTemplate>

MainViewModel

 class MethodViewModel : INotifyPropertyChanged
{
    #region Properties

    private Method _method;
    private PropertyViewModel _childViewModel;



    #endregion


    #region Getter & Setters


    public PropertyViewModel ChildViewModel
    {
        get { return this._childViewModel; }
        set
        {
            if (this._childViewModel != value)
            {
                this._childViewModel = value;
                OnPropertyChanged("ChildViewModel");
            }
        }
    }
public MethodViewModel()
        {
            //test
            _method = new Method();
            _childViewModel = new PropertyViewModel();
            _childViewModel.CollectProperties(_method.Name, _method.Helper);



            UpdateCommand = new MethodUpdateCommand(this);


        }

    public void SaveChanges()
            {

                ChildViewModel = new PropertyViewModel(_method.Name, 
      _method.Helper);

            }
    }

ChildView

class PropertyViewModel : INotifyPropertyChanged
{

    private ObservableCollection<Property> _properties;

    public ObservableCollection<Property> Properties
    {
        get { return _properties; }
        //set { _properties = value; }
    }

    public PropertyViewModel(string method, string reflection)
    {
        _properties = new ObservableCollection<Property>();

        CollectProperties(method, reflection);
    }

Property control .xaml

<StackPanel x:Name="Test" Grid.Row="1">
        <ItemsControl ItemsSource = "{Binding ChildViewModel.Properties}">
            <ItemsControl.ItemTemplate>
                <DataTemplate>



                    <StackPanel Orientation = "Horizontal">
                        <Label Content="{Binding Name}"></Label>
                        <TextBox Text = "{Binding Path, Mode=TwoWay}" 
                    Width = "100" Margin = "3 5 3 5"/>

                        <TextBox Text = "{Binding StdDev, Mode=TwoWay}"
                                 Width = "100" Margin = "3 5 3 5"/>

                        <TextBox Text = "{Binding Unit, Mode=TwoWay}"
                                 Width = "100" Margin = "3 5 3 5"/>

                    </StackPanel>

                </DataTemplate>
            </ItemsControl.ItemTemplate>
        </ItemsControl>
    </StackPanel>

My child view is updated in the debugger, but the view is not updated, I am not sure what am I missing


Solution

  • In general you solve this with ContentControls and DataTemplates. Let's say you have a MainViewModel and you want to be able to display a child view model, so you start by exposing a property with property change notification, i.e. something like this:

        private object _MyChild;
        public object MyChild
        {
            get { return this._MyChild; }
            set
            {
                if (this._MyChild != value)
                {
                    this._MyChild = value;
                    RaisePropertyChanged(() => this.MyChild);
                }
            }
        }
    

    Over in the XAML for your main window you create a content control and bind it to this property:

    <ContentControl Content="{Binding MyChild}" />
    

    Finally in your resources block you create a DataTemplate for each child view model that you might assign to this property:

    <DataTemplate DataType="{x:Type local:ChildViewModel}">
        <local:ChildViewControl />
    </DataTemplate>
    
    
    <DataTemplate DataType="{x:Type local:ChildViewModel2}">
        <local:ChildViewControl2 />
    </DataTemplate>
    
    ... etc...
    

    Ordinarily this control won't be visible, but as soon as you assign an appropriate view model to the property it will automatically populate based on what you've specified in your data templates:

    this.MyChild = new ChildViewModel(); // <-- child control gets created and appears
    

    In practice your property wouldn't be of type object, you usually have some base class that all your child view models are derived from, but you get the idea.

    There are other ways to do this (e.g. DataTriggers) but DataTemplates are what you usually use for cases such as what you've described.

    UPDATE: Here's some fully working code, imagine your MainViewModel has a property for a child view model and a couple of button handlers to set and clear the child:

    public class MainViewModel : ViewModelBase
    {
        // Child property
    
        private ChildViewModel _Child;
        public ChildViewModel Child
        {
            get { return this._Child; }
            set
            {
                if (this._Child != value)
                {
                    this._Child = value;
                    RaisePropertyChanged(() => this.Child);
                }
            }
        }
    
        // Set child
    
        private ICommand _SetChildCommand;
        public ICommand SetChildCommand => this._SetChildCommand ?? (this._SetChildCommand = new RelayCommand(OnSetChild));
    
        private void OnSetChild()
        {
            this.Child = new ChildViewModel();
        }
    
        // Clear child
    
        private ICommand _ClearChildCommand;
        public ICommand ClearChildCommand => this._ClearChildCommand ?? (this._ClearChildCommand = new RelayCommand(OnClearChild));
    
        private void OnClearChild()
        {
            this.Child = null;
        }
    
    }
    
    public class ChildViewModel : ViewModelBase
    {
        public string Text => "I am child type 1!";
    }
    

    Then in your XAML all you need to do is this:

    <StackPanel Orientation="Horizontal" VerticalAlignment="Top" >
        <Button Content="Set Child" Command="{Binding SetChildCommand}" HorizontalAlignment="Left" VerticalAlignment="Center" Margin="5" />
        <Button Content="Clear Child" Command="{Binding ClearChildCommand}" HorizontalAlignment="Left" VerticalAlignment="Center" Margin="5" />
    
    
        <ContentControl Content="{Binding Child}">
            <ContentControl.Resources>
                <DataTemplate DataType="{x:Type local:ChildViewModel}">
                    <TextBlock Text="{Binding Text}" Foreground="Yellow" Background="Blue" HorizontalAlignment="Left" VerticalAlignment="Center" />
                </DataTemplate>
            </ContentControl.Resources>
        </ContentControl>
    
    </StackPanel>
    

    Initially you'll only see the two buttons by themselves, but clicking on "Set Child" will cause the OnSetChild handler to get called, which will create a new instance of ChildViewModel and assign it to the property. Because of the DataTemplate the ContentControl will be automatically populated:

    enter image description here

    Likewise, clicking the "Clear child" button will clear the property and the yellow/blue text will disappear. (I'm using a TextBlock here but obviously you can use anything you want, including your own custom controls).