0

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();
        }
    }
}

5
  • Do you want to build a view in a separate row for each Movie instance in the list returned by GetMoviesAsync ? If so, show the XAML of this view assuming that its Data Context contains a Movie instance. It would also be helpful if you provided the code for the Movie instance. Commented Sep 12 at 20:08
  • @EldHasp I'm sorry I don't understand. I'm using the same Movie view model for both pages. Is that what you mean? Each row will be a single movie record. Commented Sep 12 at 20:14
  • In your XAML, you have <local:MovieViewModel/> specified in the Data Context. This means that the XAML is built for a MovieViewModel instance, not Movie instance. The bindings in the XAML also specify paths to the properties of the MovieViewModel instance. At the same time, in Code Behind MainVindow, you set the result of the MovieViewModel.GetAllMoviesAsync method in the Window Data Context. This method returns a list of Movie. And this list replaces the MovieViewModel created in XAML. There is no hint anywhere in your code how, where from, you get ONE Movie instance. Commented Sep 12 at 20:28
  • OK, I see now. WPF and MVVM are new for me. I started with a Movie POCO which is the movie class you mention. The Movie class is my EF entity. I then tried switching to MVVM which is when I created the MovieViewModel. So it appears I missed creating a mapping from Movie to MovieViewModel? I'll get that fixed. Commented Sep 12 at 20:46
  • FYI That private properties should be fields Commented Sep 13 at 3:28

1 Answer 1

2

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.

Sign up to request clarification or add additional context in comments.

8 Comments

You're right, I have no experience with MVVM, and there are so many different explanations/examples I got more confused. Your thorough explanation is extremely helpful and I'll have a to spend a day or so consuming it.
I had to explain it in such detail, because for the right solution, you need to correctly implement the application architecture. Therefore, knowledge and correct use of application architectural patterns (including the entire MV* family: MVC, MVP, MVVM) is very important.
@USMC6072 I'd also strongly recommend to use the CommunityToolkit.Mvvm package instead of manually writing boilerplate code like the base classes mentioned in the answer. You would for example simply annotate a private field in a view model class with the [ObservableProperty] attribute to have a property with change notification automatically generated without writing any code at all.
[deleted]
[deleted]
Clemens advice is correct. It's more a question of learning sequence. My implementation of base classes is intended for educational purposes, so that you can understand what's going on "under the hood of the black box". In practical knowledge, of course, it's better to use some common package.
@EldHasp, in the view model, why the ReloadMovies method. That method calls GetAll, why not just replace the result? Instead its comparing row by row for the diffs?
This implementation allows to speed up the rendering of the collection in View. So it leads to the replacement of the Data Context of UI elements representing individual items of the collection. BUT! If the collection is large, changes significantly and often, sometimes it is faster to replace the entire collection with a new instance of the collection. Unfortunately, there is no optimal solution "for all occasions". Therefore, perceive this as one of the options for a solution that is useful to know and often (but not always) can be useful.
Another important point. The constructor must be executed quickly, since its execution is always synchronous and will slow down the calling thread, which can lead to GUI lag. Therefore, the call to the loading method must occur asynchronously after the instance is created. Since the method is asynchronous, it is necessary to wait for its completion asynchronously. Therefore, it is optimal to set its call in an asynchronous event handler.
The event will be executed in the main application thread, and the method itself in the main thread will only perform the creation of the task and return the result of its execution. Everything else will be executed on the pool threads. Such an implementation will maximally unload the main thread, eliminate any lags and guarantee the correct saving of the result of the method execution, including even if it is interrupted by an exception.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.