Search code examples
c#wpfmvvmuser-controls

Create UserControl from another UserControl and add to parent container in runtime


I have a simple UserControl that contains a TextBox and a Button. I would like that clicking the button will create a new user control that will be positioned right after (below) the one That its button was clicked.

For example:

enter image description here

This is the code for the UserControl:

<UserControl x:Class="LearnWPF.MyUserControl"
         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" 
         d:DesignHeight="450" d:DesignWidth="800">
<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height=".9*"/>
        <RowDefinition Height=".1*"/>
    </Grid.RowDefinitions>
    <TextBox ></TextBox>
    <Button Grid.Row="1" Content="Create New UserControl" FontSize="20"/>
</Grid>

This is the main window:

<Window x:Class="LearnWPF.MyWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:myUserControl="clr-namespace:LearnWPF"
        Title="MainWindow" Height="450" Width="800">
    <StackPanel>
        <myUserControl:MyUserControl />
    </StackPanel>
</Window>

I did succeed to add new "myUserControls" to the StackPanel in the main window by doing as advised here. The 2 main problems with this implementation were:

  1. I Couldn't figure from which "myUserControl" I got the event.
  2. Even if I did know which button was click and by that where to create the new UserControl, I didn't know how to insert in the middle of the StackPanel (maybe a different panel is needed?)

Also, this solution used code behind. Is there a way to do all that in MVVM?


Solution

  • if you want to manage a list of items from a view model, then you need to use an ItemsControl instead of inserting elements to StackPanel from code-behind. Items appearance can be changed via ItemsControl.ItemTemplate. Action to add new item should be triggered by a Command bound to a button:

    <Window x:Class="XamlApp.MyWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            Title="MyWindow" WindowStartupLocation="CenterScreen"
            Height="300" Width="300">
        <Grid>
            <ScrollViewer>
                <ItemsControl ItemsSource="{Binding Path=MyItems}"
                              Margin="5"
                              Background="Wheat">
                    <ItemsControl.ItemTemplate>
                        <DataTemplate>
                            <Grid>
                                <Grid.RowDefinitions>
                                    <RowDefinition Height=".9*" />
                                    <RowDefinition Height=".1*" />
                                </Grid.RowDefinitions>
                                <TextBox Text="{Binding Path=MyText}" />
                                <Button Grid.Row="1"
                                        Content="Create New UserControl"
                                        FontSize="20"
                                        Command="{Binding Path=DataContext.AddCmd, 
                                                          RelativeSource={RelativeSource AncestorType=ItemsControl}}"
                                        CommandParameter="{Binding}"/>
                            </Grid>
                        </DataTemplate>
                    </ItemsControl.ItemTemplate>
                </ItemsControl>
            </ScrollViewer>
        </Grid>
    </Window>
    

    view model should have a collection to store the items and a command to add/insert items:

    public class StringItem
    {
        public string MyText { get; set; }
    }
    
    public class MyViewModel
    {
        public MyViewModel()
        {
            MyItems = new ObservableCollection<StringItem>();
            AddCmd = new RelayCommand<StringItem>(Add);
        }
    
        public ObservableCollection<StringItem> MyItems { get; private set; }
    
        public ICommand AddCmd { get; private set; }
    
        private void Add(StringItem current)
        {
            var item = new StringItem { MyText = "new item " + (MyItems.Count + 1) };
    
            int idx = MyItems.IndexOf(current);
            if (idx < 0)
                MyItems.Add(item);
            else 
                MyItems.Insert(idx + 1, item);
        }
    }
    

    RelayCommand is an implementation of ICommand from this lightweight MVVM package

    ViewModel and View are wired together in the View constructor:

    public partial class MyWindow : Window
    {
        public MyWindow()
        {
            InitializeComponent();
            DataContext = new MyViewModel
            {
                MyItems = { new StringItem { MyText = "hello world" } }
            };
        }
    }