.netwpfdata-binding

How do I create dynamic rows in a WPF Grid


I have a WPF app that loads a grid table through a db query. This works as expected. I want to add a tile layout, something like this. This image represents one row. enter image description here

The two different views are a separate page that load in the main window. My first problem was setting the data context on each page. I solved that by setting the data context in the main window. There are two menu buttons that will change between the views. I want to be able to display the same data when I switch pages, without making a new call to the db.

 public MainWindow()
 {
     DataContext = (new MovieViewModel()).GetMoviesAsync().Result;

     InitializeComponent();
     MovieList.DataContext = DataContext;
     MainFrame.Content = MovieList;
   }

   private void ListView_Click(object sender, RoutedEventArgs e)
   {
       MovieList.DataContext = DataContext;
       MainFrame.Content = MovieList;
   }

   private void TileView_Click(object sender, RoutedEventArgs e)
   {
       MovieGrid.DataContext = DataContext;
       MainFrame.Content = MovieGrid;
   }

The grid still populates but the tile only lists one record (understandably). I need it to dynamically build the rows based on the data context. Here is what I have in the "tile" xaml file.


<Page x:Class="MovieManager.Pages.MovieGrid"
      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:MovieManager.ViewModels"
      mc:Ignorable="d" 
      d:DesignHeight="450" d:DesignWidth="800"
      Title="MovieGrid">
    <Page.DataContext>
        <local:MovieViewModel/>
    </Page.DataContext>
    <Grid 
        Name="MovieTileLayout" ShowGridLines="False">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition  Width="*"/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition></RowDefinition>
        </Grid.RowDefinitions>
        <TextBox Name="TxtMovieName" Text="{Binding Title}" Grid.Column="0" Grid.Row="0"/>
        <TextBox Name="TxtMovieYear" Text="{Binding ReleaseDate}" Grid.Column="1" Grid.Row="0"/>

    </Grid>

</Page>

If it will help, here is my view model

using MovieManager.Models;
using MovieManager.Repository;
using System.ComponentModel;

namespace MovieManager.ViewModels
{
    public class MovieViewModel : INotifyPropertyChanged
    {
        private int movieId { get; set; }
        private int groupId { get; set; }
        private string title { get; set; }
        private string description { get; set; }
        private string[] tags { get; set; }
        private string coverImage { get; set; }
        private DateOnly releaseDate { get; set; }
        private string omdb { get; set; }
        private string groupName { get; set; }

        public int MovieId
        {
            get
            {
                return movieId;
            }
            set
            {
                if (movieId != value)
                {
                    movieId = value;
                    RaisePropertyChanged("MovieId");
                }
            }
        }
        public int GroupId
        {
            get
            {
                return groupId;
            }
            set 
            { 
                if(groupId != value)
                {
                    groupId = value;
                    RaisePropertyChanged("GroupId");
                }
            }
        }
        public string Title {
            get 
            {
                return title;
            }
            set 
            {
                if (title != value)
                {
                    title = value;
                    RaisePropertyChanged("Title");
                }
            } 
        }
        public string Description {
            get 
            {
                return description;
            }
            set { 
                if (description != value)
                {
                    description = value; 
                    RaisePropertyChanged("Description");
                }
            } 
        }
        public string[] Tags {
            get 
            {
                return tags;
            }
            set 
            { 
                if (tags != value)
                    {
                        tags = value;
                        RaisePropertyChanged("Tags");
                    }
            } 
        }
        public string CoverImage {
            get
            { 
                return coverImage;
            }
            set
            {
                if(coverImage != value)
                {
                    coverImage = value;
                    RaisePropertyChanged("CoverImage");
                }

            }
        }
        public DateOnly ReleaseDate {
            get
            {
                return releaseDate;
            }
            set
            {
                if(releaseDate != value)
                {
                    releaseDate = value;
                    RaisePropertyChanged("ReleaseDate");
                }
            }
        }
        public string OMDB {
            get
            {
                return omdb;
            }
            set
            {
                if(OMDB != value)
                {
                    omdb = value;
                    RaisePropertyChanged("OMDB");
                }
            }
        }
        public string GroupName {
            get
            { 
                return groupName;
            }
            set 
            { 
                if(groupName != value)
                {
                    groupName = value;
                    RaisePropertyChanged("GroupName");
                }
            } }


        public event PropertyChangedEventHandler PropertyChanged;

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

        public async Task<List<Movie>> GetMoviesAsync()
        {
            var repository = new MovieRepository();
            return await repository.GetAllMoviesAsync();
        }
    }
}


Solution

  • Judging by your explanations, you have little experience in implementing patterns of the «MV*» family. Therefore, my answer will go somewhat beyond the scope of your question.

    From the point of view of «MV*» patterns, your MovieRepository class is a Model. Let's assume this implementation:

        public class Movie
        {
            public int Id { get; }
            public int GroupId { get; }
            public string Title { get; }
            public string Description { get; }
            public IReadOnlyList<string> Tags { get; }
            public IReadOnlyList<byte> Image { get; }
            public DateOnly ReleaseDate { get; }
            public string OMDB { get; }
            public string GroupName { get; }
    
            public Movie(int id,
                         int groupId,
                         string? title,
                         string? description,
                         IReadOnlyList<string>? tags,
                         IReadOnlyList<byte>? image,
                         DateOnly releaseDate,
                         string? oMDB,
                         string? groupName)
            {
                Id = id;
                GroupId = groupId;
                Title = title ?? string.Empty;
                Description = description ?? string.Empty;
                Tags = Array.AsReadOnly((tags ?? Array.Empty<string>()).ToArray());
                Image = Array.AsReadOnly((image ?? Array.Empty<byte>()).ToArray());
                ReleaseDate = releaseDate;
                OMDB = oMDB ?? string.Empty;
                GroupName = groupName ?? string.Empty;
            }
        }
    
        public interface IMovieRepository
        {
            string MoviesSource { get; }
    
            Task<IEnumerable<Movie>> GetAllMoviesAsync();
        }
    
        public class MovieRepository : IMovieRepository
        {
            public string MoviesSource { get; }
    
            public MovieRepository(string moviesSource)
                => MoviesSource = moviesSource;
            public MovieRepository()
                : this("Some Default Source")
            { }
    
            public async Task<IEnumerable<Movie>> GetAllMoviesAsync()
            {
                // Some code to get Movies
    
                // Temporarily, for debugging, just an example of getting several Movies
                List<Movie> movies = await Task.Run(() =>
                {
                    List<Movie> moviesFromDB = new List<Movie>()
                    {
                        new Movie(1,1,"First", null, null, null, DateOnly.FromDateTime(DateTime.Now), null, null),
                        new Movie(3,2,"Second", null, null, null, DateOnly.FromDateTime(DateTime.Now), null, null),
                        new Movie(45,3,"Third", null, null, null, DateOnly.FromDateTime(DateTime.Now), null, null),
                        new Movie(89,1,"Fourth", null, null, null, DateOnly.FromDateTime(DateTime.Now), null, null),
                        new Movie(379,2,"Fifth", null, null, null, DateOnly.FromDateTime(DateTime.Now), null, null),
                    };
    
                    // Simulating a long method execution
                    Thread.Sleep(5);
    
                    return moviesFromDB;
                });
    
                // Returning the result
                return movies.AsReadOnly();
            }
        }
    

    To work with this Model, let's create a View Model like this:

        public class MainMoviesViewModel : BaseInpc
        {
            private readonly IMovieRepository repository;
    
            // The constructor is intended for demo mode during development.
            public MainMoviesViewModel()
                : this(new MovieRepository())
            {
                // Calling download for demo mode during development
                _ = ReLoadMovies();
            }
    
            public MainMoviesViewModel(IMovieRepository repository)
            {
                this.repository = repository;
            }
    
    
            public ObservableCollection<Movie> Movies { get; }
                = new ObservableCollection<Movie>();
    
            // I show a general case where collection update can be called multiple times,
            // as needed, during the lifetime of the MainMoviesViewModel instance.
            public async Task ReLoadMovies()
            {
                Movie[] movies = await Task.Run(async () =>
                {
                    IEnumerable<Movie> moviesResult = await repository.GetAllMoviesAsync();
                    return moviesResult.ToArray();
                });
    
                int i = 0;
    
                // Replacing entities within the overlapping collection "Movies" and array "movies".
                for (; i < movies.Length && i < Movies.Count; i++)
                {
                    Movies[i] = movies[i];
                }
    
                // Add entities if the "movies" array is longer than the "Movies" collection.
                for (; i < movies.Length; i++)
                {
                    Movies.Add(movies[i]);
                }
    
                // Remove entities if the "movies" array is shorter than the "Movies" collection.
                for (; i < Movies.Count; )
                {
                    Movies.RemoveAt(Movies.Count-1);
                }
            }
        }
    

    You can take my implementations of base classes from here: An example of my implementation of base classes: BaseInpc, RelayCommand, RelayCommandAsync, RelayCommand<T>, RelayCommandAsync<T>.

    We initialize the View Model instance in the App resources, and when the application starts, we replace it with one that is already working with actual data:

    <Application x:Class="*****.App"
                 *****************************
                 xmlns:vm="***************************"
                 StartupUri="/MainWindow.xaml"
                 Startup="OnMoviesStartup">
        <Application.Resources>
            <vm:MainMoviesViewModel x:Key="moviesVM"/>
        </Application.Resources>
    </Application>
    
        public partial class App : Application
        {
            private IMovieRepository movieRepository = null!;
            private async void OnMoviesStartup(object sender, StartupEventArgs e)
            {
                string source = "Get Source Or Const";
                movieRepository = new MovieRepository(source);
                MainMoviesViewModel moviesVM = new MainMoviesViewModel(movieRepository);
                Resources["moviesVM"] = moviesVM;
                await moviesVM.ReLoadMovies();
            }
    

    In the Window XAML we get this instance into the data context:

    <Window x:Class="**************.MainWindow"
            **************************
            DataContext="{DynamicResource moviesVM}">
    

    In the same way, this instance can be accessed in the XAML of the pages. And then there will be no need to pass it to Code Behind.

    The main WPF element for list presentation is ItemsControl (and its descendants). In this case, the simplest would be to use DataGrid:

    <Page x:Class="*******************.MovieGrid"
          **********************************
          DataContext="{DynamicResource moviesVM}">
    
        <Grid>
            <DataGrid ItemsSource="{Binding Movies}"/>
        </Grid>
    </Page>
    

    DataGrid (like any UI element in WPF) has very large customization capabilities. But if you need to build a view based on rows, you can set a data template for ItemsControl (or ListBox):

        <Grid>
            <ListBox ItemsSource="{Binding Movies}">
                <ItemsControl.ItemTemplate>
                    <DataTemplate>
                        <Grid>
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition/>
                                <ColumnDefinition/>
                            </Grid.ColumnDefinitions>
                            <TextBox Name="TxtMovieName" Text="{Binding Title, Mode=OneWay}" Grid.Column="0" Grid.Row="0"/>
                            <TextBox Name="TxtMovieYear" Text="{Binding ReleaseDate, Mode=OneWay}" Grid.Column="1" Grid.Row="0"/>
                        </Grid>
                    </DataTemplate>
                </ItemsControl.ItemTemplate>
            </ListBox>
        </Grid>
    

    To align the sizes of Grid columns and rows, «IsSharedSizeScope» and «SharedSizeGroup» is used:

            <ListBox ItemsSource="{Binding Movies}"
                     Grid.IsSharedSizeScope="True">
                <ItemsControl.ItemTemplate>
                    <DataTemplate>
                        <Grid>
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition SharedSizeGroup="title"/>
                                <ColumnDefinition SharedSizeGroup="release"/>
                            </Grid.ColumnDefinitions>
                            <TextBox Name="TxtMovieName" Text="{Binding Title, Mode=OneWay}" Grid.Column="0" Grid.Row="0"/>
                            <TextBox Name="TxtMovieYear" Text="{Binding ReleaseDate, Mode=OneWay}" Grid.Column="1" Grid.Row="0"/>
                        </Grid>
                    </DataTemplate>
                </ItemsControl.ItemTemplate>
            </ListBox>
    

    P.S. Although your question is not directly related to the implementation of DB editing, judging by your code, you have such a task.
    My answer uses "Read Only" entities, and to implement editing, it is necessary to provide the corresponding methods in the repository. Another important point is updating entities for the view. In my example, this is only possible through a complete reboot of all entities. But in more professional implementations, it is better to provide an event (or events) in the repository notifying about a change in some entity or about its addition or deletion.
    If this is done, then it will be necessary to add a shell over Movie at the View Model level, in which there will be properties available for editing. But this is a separate and large topic for discussion.