Home > Software design >  Dynamically add User Controls based on List in WPF with MVVM pattern
Dynamically add User Controls based on List in WPF with MVVM pattern

Time:06-10

I have two views for my application. Looking at the first View, UpdaterMainView, you can see that I create a Grid with two columns. The left column is split even further, into 5 rows. What I want to do is fill the five rows from a list of objects that are stored in the UpdaterMainViewModel, TaskList. I created a custom User Control called TaskView which is the layout of how I want to display the information of the list, a button, and a textbox.

When I instantiate the UpdaterMainViewModel, my application scans a directory for a few things and creates a list of tasks to complete. There are only five total possible tasks, hence why I created five rows. If the conditions aren't met and Task 2 doesn't need to be run, I don't want to show the UserControl and the one below it, should move up. I don't want to use the code-behind unless it can still meet the guidelines of MVVM.

For testing, I added <local:TaskView Grid.Column="0" Grid.Row="0"/> to the UpdaterMainView and created a default constructor. But, what I need, is to add TaskView for each item in TaskList. To get this to work, I also have to create the UserControls with a different constructor. When you use DataContext, it uses a parameterless constructor. I somehow need to use the constructor with the enum parameter to fill the grid with each appropriate TaskView.

UpdaterMainView:

<UserControl x:Class="POSUpdaterGUI.View.UpdaterMainView"
             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:POSUpdaterGUI.View" xmlns:local1="clr-namespace:POSUpdaterGUI.ViewModel"
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800">
    <UserControl.DataContext>
        <local1:UpdaterMainViewModel/>
    </UserControl.DataContext>
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="200" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>

        <Grid Grid.Row="0">
            <Grid.RowDefinitions>
                <RowDefinition Height="60" />
                <RowDefinition Height="60" />
                <RowDefinition Height="60" />
                <RowDefinition Height="60" />
                <RowDefinition Height="60" />
            </Grid.RowDefinitions>

            <local:TaskView Grid.Column="0" Grid.Row="0"/>
            <local:TaskView Grid.Column="0" Grid.Row="1"/>
            <local:TaskView Grid.Column="0" Grid.Row="2"/>
            <local:TaskView Grid.Column="0" Grid.Row="3"/>
        </Grid>

        <TextBox Grid.Column="1" Margin="10" TextWrapping="Wrap" Text="{Binding OutputText}"/>

    </Grid>
</UserControl>

UpdaterMainViewModel:

using POSUpdaterLibrary;
using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace POSUpdaterGUI.ViewModel
{
    public class UpdaterMainViewModel : INotifyPropertyChanged
    {
        private string outputText;

        public string OutputText
        {
            get => outputText;
            set
            {
                outputText = value;
                NotifyPropertyChanged("OutputText");
            }
        }

        public UpdateTaskList TaskList { get; set; }

        public UpdaterMainViewModel()
        {
            Log("Application Starting.");
            TaskList = new UpdateTaskList();
            TaskList.LoggerEvent  = TaskList_LoggerEvent;
            TaskList.GetList(AppContext.BaseDirectory);
        }

        private void TaskList_LoggerEvent(object sender, LoggerEventArgs e)
        {
            Log(e.LogString);
        }

        private void Log(string message)
        {
            if (OutputText == null)
            {
                OutputText = DateTime.Now.ToString()   " - "   message;
                return;
            }
            OutputText  = "\n"   DateTime.Now.ToString()   " - "   message;
        }

        public event PropertyChangedEventHandler PropertyChanged;

        private void NotifyPropertyChanged([CallerMemberName] string name = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
        }
    }
}

TaskView:

<UserControl x:Class="POSUpdaterGUI.View.TaskView"
         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:POSUpdaterGUI.View" xmlns:local1="clr-namespace:POSUpdaterGUI.ViewModel"
         mc:Ignorable="d" 
         d:DesignHeight="60" d:DesignWidth="500">

<UserControl.DataContext>
    <local1:TaskViewModel/>
</UserControl.DataContext>

<Grid>
    <Grid.ColumnDefinitions>
        <ColumnDefinition  Width="60"/>
        <ColumnDefinition  Width="*"/>
    </Grid.ColumnDefinitions>

    <Button Grid.Column="0" HorizontalAlignment="Center" Margin="0,0,0,0" VerticalAlignment="Center" Height="50" Width="50" Command="{Binding RunTaskCommand}">
        <Image Source="{Binding StatusImage}"/>
    </Button>
    <Label Grid.Column="1" Content="{Binding TaskName}" HorizontalAlignment="Left" Margin="15,0,0,0" VerticalAlignment="Center"/>
</Grid>

TaskViewModel:

using POSUpdaterLibrary;
using System;
using System.ComponentModel;
using System.Windows.Input;

namespace POSUpdaterGUI.ViewModel
{
    class TaskViewModel : INotifyPropertyChanged
    {
        public string TaskName { get; set; }

        public ICommand RunTaskCommand { get; set; }

        public string StatusImage { get; set; }

        public TaskViewModel()
        {
            TaskName = "undefined";
            RunTaskCommand = new RelayCommand(new Action<object>(DefaultMethod));
            StatusImage = STARTIMAGE;
        }

        public TaskViewModel(UpdaterConstants.TaskType taskType)
        {
            switch (taskType)
            {
                case UpdaterConstants.TaskType.KillProcesses:
                    TaskName = "Processes Killed";
                    RunTaskCommand = new RelayCommand(new Action<object>(DefaultMethod));
                    StatusImage = STARTIMAGE;
                    break;
                case UpdaterConstants.TaskType.FileCopy:
                    TaskName = "Files Copied";
                    RunTaskCommand = new RelayCommand(new Action<object>(DefaultMethod));
                    StatusImage = STARTIMAGE;
                    break;
                case UpdaterConstants.TaskType.DatabaseUpdates:
                    TaskName = "Database Updated";
                    RunTaskCommand = new RelayCommand(new Action<object>(DefaultMethod));
                    StatusImage = STARTIMAGE;
                    break;
                case UpdaterConstants.TaskType.COMDLLsRegistered:
                    TaskName = "COM DLLs Registered";
                    RunTaskCommand = new RelayCommand(new Action<object>(DefaultMethod));
                    StatusImage = STARTIMAGE;
                    break;
                case UpdaterConstants.TaskType.FileCleanup:
                    TaskName = "Files Cleaned";
                    RunTaskCommand = new RelayCommand(new Action<object>(DefaultMethod));
                    StatusImage = STARTIMAGE;
                    break;
                default:
                    break;
            }

        }

        public enum Status
        {
            START,
            COMPLETE,
            FAILED,
            WAIT
        }

        internal const string STARTIMAGE = "/data/start.png";
        internal const string COMPLETEIMAGE = "/data/complete.png";
        internal const string FAILEDIMAGE = "/data/failed.png";
        internal const string WAITIMAGE = "/data/wait.png";

        public event PropertyChangedEventHandler PropertyChanged;

        private void DefaultMethod(object obj)
        {
            // Shouldn't be shown. Just a hold-over
            throw new NotImplementedException();
        }
    }
}

The UpdaterMainView is the main view of the application and is always showing. To prove this, here is my MainWindow.xaml:

<Window
    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:POSUpdaterGUI"
    xmlns:View="clr-namespace:POSUpdaterGUI.View" x:Class="POSUpdaterGUI.MainWindow"
    mc:Ignorable="d"
    Title="MainWindow" Height="450" Width="800">
<Grid>
    <View:UpdaterMainView/>
</Grid>

CodePudding user response:

Thanks @Clemens for pointing me in the right direction! I did not need a separate View. I did need to use Data Templating.

Here is what I ended up with for the UpdaterMainView:

<UserControl x:Class="POSUpdaterGUI.View.UpdaterMainView"
             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:POSUpdaterGUI.View" xmlns:local1="clr-namespace:POSUpdaterGUI.ViewModel"
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800">
   
    <UserControl.DataContext>
        <local1:UpdaterMainViewModel/>
    </UserControl.DataContext>
    
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="250" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>

        <ListBox Grid.Column="0" Margin="10" ItemsSource="{Binding TaskList}">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <StackPanel>
                        <Grid>
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition  Width="60"/>
                                <ColumnDefinition  Width="*"/>
                            </Grid.ColumnDefinitions>

                            <Button Grid.Column="0" HorizontalAlignment="Center" Margin="0,0,0,0" VerticalAlignment="Center" Height="50" Width="50" Command="{Binding Command}">
                                <Image Source="{Binding StatusImage}"/>
                            </Button>
                            <Label Grid.Column="1" Content="{Binding TaskName}" HorizontalAlignment="Left" Margin="15,0,0,0" VerticalAlignment="Center"/>
                        </Grid>
                    </StackPanel>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>

        <TextBox Grid.Column="1" Margin="10" TextWrapping="Wrap" Text="{Binding OutputText}"/>

    </Grid>
</UserControl>

Here is what I ended up with for the UpdaterMainViewModel:

using POSUpdaterGUI.Models;
using POSUpdaterLibrary;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace POSUpdaterGUI.ViewModel
{
    public class UpdaterMainViewModel : INotifyPropertyChanged
    {
        private UpdaterLogger _logger;
        private string outputText;

        public string OutputText
        {
            get => outputText;
            set
            {
                outputText = value;
                NotifyPropertyChanged("OutputText");
            }
        }

        private List<UpdateTaskWrapper> taskList;

        public List<UpdateTaskWrapper> TaskList 
        { 
            get => taskList;
            set
            {
                taskList = value;
                NotifyPropertyChanged("TaskList");
            }
        }

        public UpdaterMainViewModel()
        {
            _logger = UpdaterLogger.Instance;
            _logger.LoggerEvent  = Logger_LoggerEvent;
            Log("Application Starting.");
            TaskList = GetTaskList();
        }

        private void Logger_LoggerEvent(object sender, LoggerEventArgs e)
        {
            Log(e.LogString);
        }

        private List<UpdateTaskWrapper> GetTaskList()
        {
            UpdateTaskList list = new UpdateTaskList();
            var myList = list.GetList(AppContext.BaseDirectory);


            // Create one out of the Wrapper class.
            List<UpdateTaskWrapper> wrappedList = new List<UpdateTaskWrapper>();

            foreach (var task in myList)
            {
                wrappedList.Add(new UpdateTaskWrapper(task));
            }

            return wrappedList;
        }

        private void Log(string message)
        {
            if (OutputText == null)
            {
                OutputText = DateTime.Now.ToString()   " - "   message;
                return;
            }
            OutputText  = "\n"   DateTime.Now.ToString()   " - "   message;
        }

        public event PropertyChangedEventHandler PropertyChanged;

        private void NotifyPropertyChanged([CallerMemberName] string name = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
        }
    }
}

The most important parts were that I removed the TaskView, Added Datatemplating (between the tags), and changed the List from a custom UpdaterTaskList to List.

  • Related