I am very new to WPF and trying to create a self learning application using WPF.
I am struggling to understand concepts like data binding,data templates,ItemControls to a full extent.
I am trying to create a learn page with the following requirements in mind.
1) The page can have more than one question.A scroll bar should be displayed once the
questions fills up the whole page.
2) The format of the choices vary based on the question type.
3) the user shall be able to select the answer for the question.
I am facing problem with binding nested ObservableCollection and displaying the content as for the above requirements.
Can someone help in how to create a page as shown below and how to use INotifyPropertyChanged along XMAL to do the nested binding.
Here is the basic code I am trying to use to use display the questions and answers.
namespace Learn
{
public enum QuestionType
{
OppositeMeanings,
LinkWords
//todo
}
public class Question
{
public Question()
{
Choices = new ObservableCollection<Choice>();
}
public string Name { set; get; }
public string Instruction { set; get; }
public string Clue { set; get; }
public ObservableCollection<Choice> Choices { set; get; }
public QuestionType Qtype { set; get; }
public Answer Ans { set; get; }
public int Marks { set; get; }
}
}
namespace Learn
{
public class Choice
{
public string Name { get; set; }
public bool isChecked { get; set; }
}
}
namespace Learn
{
public class NestedItemsViewModel
{
public NestedItemsViewModel()
{
Questions = new ObservableCollection<Question>();
for (int i = 0; i < 10; i++)
{
Question qn = new Question();
qn.Name = "Qn" + i;
for (int j = 0; j < 4; j++)
{
Choice ch = new Choice();
ch.Name = "Choice" + j;
qn.Choices.Add(ch);
}
Questions.Add(qn);
}
}
public ObservableCollection<Question> Questions { get; set; }
}
public partial class LearnPage : UserControl
{
public LearnPage()
{
InitializeComponent();
this.DataContext = new NestedItemsViewModel();
}
}
}
You initial attempt gets you 80% of the way there. Hopefully, my answer will get you a little closer.
To start with, INotifyPropertyChanged is an interface an object supports to notify the Xaml engine that data has been modified and the user interface needs to be updated to show the change. You only need to do this on standard clr properties.
So if your data traffic is all one way, from the ui to the model, then there is no need for you to implement INotifyPropertyChanged.
I have created an example that uses the code you supplied, I have modified it and created a view to display it. The ViewModel and the data classes are as follows
public enum QuestionType
{
OppositeMeanings,
LinkWords
}
public class Instruction
{
public string Name { get; set; }
public ObservableCollection<Question> Questions { get; set; }
}
public class Question : INotifyPropertyChanged
{
private Choice selectedChoice;
private string instruction;
public Question()
{
Choices = new ObservableCollection<Choice>();
}
public string Name { set; get; }
public bool IsInstruction { get { return !string.IsNullOrEmpty(Instruction); } }
public string Instruction
{
get { return instruction; }
set
{
if (value != instruction)
{
instruction = value;
OnPropertyChanged();
OnPropertyChanged("IsInstruction");
}
}
}
public string Clue { set; get; }
public ObservableCollection<Choice> Choices { set; get; }
public QuestionType Qtype { set; get; }
public Choice SelectedChoice
{
get { return selectedChoice; }
set
{
if (value != selectedChoice)
{
selectedChoice = value;
OnPropertyChanged();
}
}
}
public int Marks { set; get; }
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
var handler = PropertyChanged;
if (handler != null)
{
handler.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
public class Choice
{
public string Name { get; set; }
public bool IsCorrect { get; set; }
}
public class NestedItemsViewModel
{
public NestedItemsViewModel()
{
Questions = new ObservableCollection<Question>();
for (var h = 0; h <= 1; h++)
{
Questions.Add(new Question() { Instruction = string.Format("Instruction {0}", h) });
for (int i = 1; i < 5; i++)
{
Question qn = new Question() { Name = "Qn" + ((4 * h) + i) };
for (int j = 0; j < 4; j++)
{
qn.Choices.Add(new Choice() { Name = "Choice" + j, IsCorrect = j == i - 1 });
}
Questions.Add(qn);
}
}
}
public ObservableCollection<Question> Questions { get; set; }
internal void SelectChoice(int questionIndex, int choiceIndex)
{
var question = this.Questions[questionIndex];
question.SelectedChoice = question.Choices[choiceIndex];
}
}
Notice that Answer has been changed to a SelectedChoice. This may not be what you require but it made the example a little easier. i have also implemented the INotifyPropertyChanged pattern on the SelectedChoice so I can set the SelectedChoice from code (notably from a call to SelectChoice).
The main windows code behind instantiates the ViewModel and handles a button event to set a choice from code behind (purely to show INotifyPropertyChanged working).
public partial class MainWindow : Window
{
public MainWindow()
{
ViewModel = new NestedItemsViewModel();
InitializeComponent();
}
public NestedItemsViewModel ViewModel { get; set; }
private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
ViewModel.SelectChoice(3, 3);
}
}
The Xaml is
<Window x:Class="StackOverflow._20984156.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:learn="clr-namespace:StackOverflow._20984156"
DataContext="{Binding RelativeSource={RelativeSource Self}, Path=ViewModel}"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<learn:SelectedItemIsCorrectToBooleanConverter x:Key="SelectedCheckedToBoolean" />
<Style x:Key="ChoiceRadioButtonStyle" TargetType="{x:Type RadioButton}" BasedOn="{StaticResource {x:Type RadioButton}}">
<Style.Triggers>
<DataTrigger Value="True">
<DataTrigger.Binding>
<MultiBinding Converter="{StaticResource SelectedCheckedToBoolean}">
<Binding Path="IsCorrect" />
<Binding RelativeSource="{RelativeSource Self}" Path="IsChecked" />
</MultiBinding>
</DataTrigger.Binding>
<Setter Property="Background" Value="Green"></Setter>
</DataTrigger>
<DataTrigger Value="False">
<DataTrigger.Binding>
<MultiBinding Converter="{StaticResource SelectedCheckedToBoolean}">
<Binding Path="IsCorrect" />
<Binding RelativeSource="{RelativeSource Self}" Path="IsChecked" />
</MultiBinding>
</DataTrigger.Binding>
<Setter Property="Background" Value="Red"></Setter>
</DataTrigger>
</Style.Triggers>
</Style>
<DataTemplate x:Key="InstructionTemplate" DataType="{x:Type learn:Question}">
<TextBlock Text="{Binding Path=Instruction}" />
</DataTemplate>
<DataTemplate x:Key="QuestionTemplate" DataType="{x:Type learn:Question}">
<StackPanel Margin="10 0">
<TextBlock Text="{Binding Path=Name}" />
<ListBox ItemsSource="{Binding Path=Choices}" SelectedItem="{Binding Path=SelectedChoice}" HorizontalAlignment="Stretch">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type learn:Choice}">
<RadioButton Content="{Binding Path=Name}" IsChecked="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ListBoxItem}}, Path=IsSelected}" Margin="10 1"
Style="{StaticResource ChoiceRadioButtonStyle}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</StackPanel>
</DataTemplate>
</Window.Resources>
<DockPanel>
<StackPanel Orientation="Horizontal" DockPanel.Dock="Bottom">
<Button Content="Select Question 3 choice 3" Click="ButtonBase_OnClick" />
</StackPanel>
<ItemsControl ItemsSource="{Binding Path=Questions}">
<ItemsControl.ItemTemplateSelector>
<learn:QuestionTemplateSelector QuestionTemplate="{StaticResource QuestionTemplate}" InstructionTemplate="{StaticResource InstructionTemplate}" />
</ItemsControl.ItemTemplateSelector>
</ItemsControl>
</DockPanel>
</Window>
Note: My learn namespace is different from yours so if you use this code, you will need to modify it to your namespace.
So, the primary ListBox display a list of Questions. Each item in the ListBox (each Question) is rendered using a DataTemplate. Similarly, in the DataTemplate, a ListBox is used to display the choices and a DataTemplate is used to render each choice as a radio button.
Points of interest.
Each choice is bound to the IsSelected property of the ListBoxItem it belongs to. It may not appear in the xaml but there will be a ListBoxItem for each choice. The IsSelected property is kept in sync with the SelectedItem property of the ListBox (by the ListBox) and that is bound to the SelectedChoice in your question.
The choice ListBox has an ItemsPanel. This allows you to use the layout strategy of a different type of panel to layout the items of the ListBox. In this case, a horizontal StackPanel.
I have added a button to set the choice of question 3 to 3 in the viewmodel. This will show you INotifyPropertyChanged working. If you remove the OnPropertyChanged call from the setter of the SelectedChoice property, the view will not reflect the change.
The example above does not handle the Instruction Type.
To handle instructions, I would either
Insert the instruction as a question and change the question DataTemplate so it does not display the choices for an instruction; or
Create a collection of Instructions in the view model where the Instruction type has a collection of questions (the view model would no longer have a collection of questions).
The Instruction class would be something like
public class Instruction
{
public string Name { get; set; }
public ObservableCollection<Question> Questions { get; set; }
}
Addition based on comment regarding timer expiration and multiple pages.
The comments here are aimed at giving you enough information to know what to search for.
INotifyPropertyChanged
If in doubt, implement INotifyPropertyChanged. My comment above was to let you know why you use it. If you have data already displayed that will be manipulated from code, then you must implement INotifyPropertyChanged.
The ObservableCollection object is awesome for handling the manipulation of lists from code. Not only does it implement INotifyPropertyChanged, but it also implements INotifyCollectionChanged, both of these interfaces ensure that if the collection changes, the xaml engine knows about it and displays the changes. Note that if you modify a property of an object in the collection, it will be up to you to notify the Xaml engine of the change by implementing INotifyPropertyChanged on the object. The ObservableCollection is awesome, not omnipercipient.
Paging
For your scenario, paging is simple. Store the complete list of questions somewhere (memory, database, file). When you go to page 1, query the store for those questions and populate the ObservableCollection with those questions. When you go to page 2, query the store for page 2 questions, CLEAR the ObservableCollection and re populate. If you instantiate the ObservableCollection once and then clear and repopulate it while paging, the ListBox refresh will be handled for you.
Timers
Timers are pretty resource intensive from a windows point of view and as such, should be used sparingly. There are a number of timers in .net you can use. I tend to play with System.Threading.Timer or System.Timers.Timer. Both of these invoke the timer callback on a thread other than the DispatcherThread, which allows you to do work without affecting the UI responsiveness. However, if during the work you need to modify the UI, you will need to Dispatcher.Invoke or Dispatcher.BeginInvoke to get back on the Dispatcher thread. BeginInvoke is asynchronous and therefore, should not hang the thread while it waits for the DispatcherThread to become idle.
Addition based on comment regarding separation of data templates.
I added an IsInstruction to the Question object (I did not implement a Instruction class). This shows an example of raising the PropertyChanged event from property A (Instruction) for Property B (IsInstruction).
I moved the DataTemplate from the list box to the Window.Resources and gave it a key. I also created a second DataTemplate for the instruction items.
I created a DataTemplateSelector to choose which DataTemplate to use. DataTemplateSelectors are good when you need to select a DataTemplate as the Data is being loaded. Consider it a OneTime selector. If you required the DataTemplate to change during the scope of the data it is rendering, then you should use a trigger. The code for the selector is
public class QuestionTemplateSelector : DataTemplateSelector
{
public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
DataTemplate template = null;
var question = item as Question;
if (question != null)
{
template = question.IsInstruction ? InstructionTemplate : QuestionTemplate;
if (template == null)
{
template = base.SelectTemplate(item, container);
}
}
else
{
template = base.SelectTemplate(item, container);
}
return template;
}
public DataTemplate QuestionTemplate { get; set; }
public DataTemplate InstructionTemplate { get; set; }
}
The selector is bound to the ItemTemplateSelector of the ItemsControl.
Finally, I converted the ListBox into an ItemsControl. The ItemsControl has most of the functionality of a ListBox (the ListBox control is derived from an ItemsControl) but it lacks the Selected functionality. It will make your questions seem more like a page of questions than a list.
NOTE: Although I only added the code of the DataTemplateSelector to the addition, I updated the code snipits throughout the rest of the answer to work with the new DataTemplateSelector.
Addition based on comment regarding setting the background for right and wrong answers
Setting the background dynamically based on values in the model requires a trigger, in this case, multiple triggers.
I have updated the Choice object to include an IsCorrect and during the creation of the questions in the ViewModel, I have assigned IsCorrect on one of the Choices for each answer.
I have also updated the MainWindow to include style triggers on the RadioButton. There are a few points to not about triggers
1. The style or the RadioButton sets the backgound when the mouse is over. A fix would require recreating the style of the RadioButton.
1. Since the trigger is based on 2 values, we can either create another property on the model to combine the 2 properties, or use MultiBinding and a MultValueConverter. I have used the MultiBinding and the MultiValueConverter is as follows.
public class SelectedItemIsCorrectToBooleanConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
var boolValues = values.OfType<bool>().ToList();
var isCorrectValue = boolValues[0];
var isSelected = boolValues[1];
if (isSelected)
{
return isCorrectValue;
}
return DependencyProperty.UnsetValue;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
I hope this helps
Related
Summary
I've got an element within a data template, that I want bound to some property of the main data context.
I realise that in this specific situation, a different solution may be preferable (and I have a working solution that avoids this), but I suspect this kind of problem may come up again and I want to know how to solve it in the general case.
Below are the specifics of my situation.
The Details
Data Hierarchy: I have a list of type A, each instance of A has a list of type B, each instance of B has some other data including a string for a text log.
UI Structure: I have a ComboBox to select an item of type A. I have a TabControl with the tabs representing items of type B, taken from the selected A above. In each tab, there is a means to enter data to populate the object of type B, and a log, representing changes to that instance of B.
Backing Logic: I track the selected item in each list with properties (SelectionA and SelectionB in the data context, MainWindowViewModel) that notify when they change. The B object also notifies when its log text changes. These ensure that the UI responds to changes to the backing data.
Problem: I want to move the notify logic to all be in one place (the DataContext, i.e. MainWindowViewModel), rather than having some in the B class and needing to duplicate the notify logic. To achieve this, I add a property (SelectionBLogText) to track the LogText property of the SelectionB object, and bind the log (in the templated tabpanel) to the main SelectionBLogText property. The problem is that within the tabpage, I can only seem to bind to properties of the selected B object (from the selected tab), and I need to bind to a property of the DataContext instead. I've tried using RelativeSource but nothing I've tried so far works, and the more I look at the docs the more I feel it's designed for another job.
The XAML (with irrelevant details removed):
<Window x:Class="WPFQuestion.MainWindow"
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:WPFQuestion"
mc:Ignorable="d"
Title="MainWindow"
Height="350"
Width="930">
<DockPanel>
<ComboBox
ItemsSource="{Binding ListOfA}"
SelectedItem="{Binding SelectionA}"
DisplayMemberPath="Name"/>
<TabControl
ItemsSource="{Binding SelectionA}"
SelectedItem="{Binding SelectionB}"
DisplayMemberPath="Name">
<TabControl.ContentTemplate>
<ItemContainerTemplate>
<StackPanel>
<TextBox
IsReadOnly="True"
Text="{Binding Path=???.SelectionBLogText}"/>
<Button Click="ClearLogButton_Click"/>
</StackPanel>
</ItemContainerTemplate>
</TabControl.ContentTemplate>
</TabControl>
</DockPanel>
</Window>
And the code-behind:
public partial class MainWindow : Window
{
internal MainWindowViewModel vm;
public MainWindow()
{
InitializeComponent();
vm = new MainWindowViewModel();
DataContext = vm;
}
// Various methods for event handling
}
public class A : IEnumerable<B>
{
public string Name { get; set; }
public List<B> Bs { get; set; }
}
public class B // previously : INotifyPropertyChanged
{
public string Name { get; set; }
public string LogText { get; set; }
// various other properties
}
public class MainWindowViewModel : INotifyPropertyChanged
{
private A _a;
private B _b;
public event PropertyChangedEventHandler PropertyChanged;
public List<A> ListOfA { get; set; }
public A SelectionA
{
get => _a;
set
{
if (_a == value)
{
return;
}
_a = value;
RaisePropertyChanged(nameof(SelectionA));
}
}
public B SelectionB
{
get => _b;
set
{
if (_b == value)
{
return;
}
_b = value;
RaisePropertyChanged(nameof(SelectionB));
RaisePropertyChanged(nameof(SelectionBLogText));
}
}
public string SelectionBLogText
{
get => SelectionB.LogText;
set
{
if (SelectionB.LogText == value)
{
return;
}
SelectionB.LogText = value;
RaisePropertyChanged(nameof(SelectionBLogText));
}
}
private void RaisePropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
have you tried something like this when you used relative binding? if not please check this out.
<TextBox IsReadOnly="True"
Text="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=Window},
Path=Datacontext.SelectionBLogText}"/>
I have a TabControl with multiple DataTemplate. the first DataTemplate will be used for search reasons and the second will be for displaying items obtained from that search. My XAML code will be as follows:
<UserControl.Resources>
<!--First template-->
<DataTemplate>
<!-- I will have a DataGrid here-->
</DataTemplate>
<!--Second template-->
<DataTemplate >
<!-- I will have details of one item of the DataGrid-->
</DataTemplate>
</UserControl.Resources>
<TabControl ItemsSource="{Binding }"/>
What I want to accomplish is that in the TabControl the first tab will contain the first DataTemplate (the search template) and when I double click on one row of my DataGrid, a tab will be added with the details of that row (in other words a tab with the second template).
Since I am using MVVM, I thought of creating two UserControls, one for each template and then catch the double click event, but after this I don't know how to add a tab since now my search template is a UserControl seperated from the one that contains the TabControl.
So how do I do this?
UPDATE:
As I read the answers I think I wasn't very clear in stating the problem.
My problem is how to add tabs with the second template, by catching double click events from the first template. I don't have any problem in adding the two templates independently.
Rather can creating two UserControls, you can create and use a DataTemplateSelector in order to switch different DataTemplates in.
Basically, create a new class that inhereits from DataTemplateSelector and override the SelecteTemplate method. Then declare an instance of it in the XAML (much like a value converter), and then apply it to ContentTemplateSelector property of the TabControl.
More info can be found here.
If you're going to do this with MVVM, your tab control should be bound to some ObservableCollection in your VM, and you just add and remove VM's to the collection as needed.
The VMs can be any type you like and your DataTemplates will show the correct view in the tab just like any other view, so yes, create two UserControls for the two views.
public class MainVM
{
public ObservableCollection<object> Views { get; private set; }
public MainVM()
{
this.Views = new ObservableCollection<object>();
this.Views.Add(new SearchVM(GotResults));
}
private void GotResults(Results results)
{
this.Views.Add(new ResultVM(results));
}
}
There are two options: Use datatemplate selector, or use implicit datatemplates and different types for each tabitem.
1. DataTemplateSelector:
public ObservableCollection<TabItemVM> Tabs { get; private set; }
public MainVM()
{
Tabs = ObservableCollection<TabItemVM>
{
new TabItemVM { Name="Tab 1" },
};
}
void AddTab(){
var newTab = new TabItemVM { Name="Tab 2" };
Tabs.Add(newTab);
//SelectedTab = newTab; //you may bind TabControl.SelectedItemProperty to viewmodel in order to be able to activate the tab from viewmodel
}
public class TabItemTemplateSelector : DataTemplateSelector
{
public DataTemplate Tab1Template { get; set; }
public DataTemplate Tab2Template { get; set; }
public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
var tabItem = item as TabItemVM;
if (tabItem.Name == "Tab 1") return Tab1Template;
if (tabItem.Name == "Tab 2") return Tab2Template;
return base.SelectTemplate(item, container);
}
}
<local:TabItemTemplateSelector
x:Key="TabItemTemplateSelector"
Tab1Template="{StaticResource Tab1Template}"
Tab2Template="{StaticResource Tab2Template}" />
2. Implicit Data Templates:
public class MainVM : ViewModelBase
{
public ObservableCollection<TabItemVM> Tabs { get; private set; }
public MainVM()
{
Tabs = new ObservableCollection<TabItemVM>
{
new Tab1VM(),
};
}
void AddTab()
{
var newTab = new Tab2VM()
Tabs.Add(newTab);
//SelectedTab = newTab;
}
}
public class TabItemBase
{
public string Name { get; protected set; }
}
public class Tab1VM : TabItemBase
{
public Tab1VM()
{
Name = "Tab 1";
}
}
public class Tab2VM : TabItemBase
{
public Tab2VM()
{
Name = "Tab 2";
}
}
<UserControl.Resources>
<!--First template-->
<DataTemplate DataType="local:Tab1VM">
<!-- I will have a DataGrid here-->
</DataTemplate>
<!--Second template-->
<DataTemplate DataType="local:Tab2VM">
<!-- I will have details of one item of the DataGrid-->
</DataTemplate>
</UserControl.Resources>
I am using Entity Framework Code First
I have a Movie like so:
public class Movie
{
public byte[] Thumbnail { get; set; }
public int MovieId { get; set; }
}
And a Collection of Movies like so:
public class NorthwindContext : DbContext
{
public DbSet<Movie> Movies { get; set; }
}
I have a MovieViewModel like so:
public class MovieViewModel
{
private readonly Movie _movie;
public MovieViewModel(Movie movie)
{
_movieModel = movieModel;
}
public byte[] Thumbnail { get { return _movie.Thumbnail; } }
}
When my App starts:
public ObservableCollection<MovieViewModel> MovieVms =
new ObservableCollection<MovieViewModel>();
foreach (var movie in MyDbContext.Movies)
MovieVms.Add(new MovieViewModel(movie));
I have 4000 movies. This process takes 25 seconds. Is there a better/faster way to do this?
My main page uses the thumbnails like so, but to be clear this loading time happens before anything UI related:
MyView = new ListCollectionView(MovieVms);
<ListBox ItemsSource="{Binding MyView}" />
Also my memory usage goes through the roof. How should I be loading these images? I need a full collection of view models off the bat to enable sorting, filtering, searching, but I only need the thumbnails of the items visible in my wrap panel.
EDIT---
Thanks Dave for a great answer. Can you elaborate on "make it an association (aka navigation property)"
var thumbnail = new Thumbnail();
thumbnail.Data = movie.GetThumbnail();
Globals.DbContext.Thumbnails.Add(thumbnail);
Globals.DbContext.SaveChanges();
movie.ThumbnailId = thumbnail.ThumbnailId;
Globals.DbContext.SaveChanges();
I can run that code with no errors, but my property in my MovieViewModel
public new byte[] Thumbnail { get { return _movie.Thumbnail.Data; } }
always has a null Thumbnail and errors as soon as my UI accesses it. A breakpoint on movie.ThumbnailId is never hit. Do I have to load the association manually?
I think you are essentially asking how to do several different things:
Load the entire list of movies quickly, to allow for sorting and filtering in the UI
Display the movie thumbnails in the UI but only when they are scrolled into view
Keep memory usage to a minimum
Display the UI as quickly as possible after the application starts
Load the movies quickly
First off, as #Dave M's answer states, you need to split the thumbnail into a separate entity so that you can ask Entity Framework to load the list of movies without also loading the thumbnails.
public class Movie
{
public int Id { get; set; }
public int ThumbnailId { get; set; }
public virtual Thumbnail Thumbnail { get; set; } // This property must be declared virtual
public string Name { get; set; }
// other properties
}
public class Thumbnail
{
public int Id { get; set; }
public byte[] Image { get; set; }
}
public class MoviesContext : DbContext
{
public MoviesContext(string connectionString)
: base(connectionString)
{
}
public DbSet<Movie> Movies { get; set; }
public DbSet<Thumbnail> Thumbnails { get; set; }
}
So, to load all of the movies:
public List<Movie> LoadMovies()
{
// Need to get '_connectionString' from somewhere: probably best to pass it into the class constructor and store in a field member
using (var db = new MoviesContext(_connectionString))
{
return db.Movies.AsNoTracking().ToList();
}
}
At this point you will have a list of Movie entities where the ThumbnailId property is populated but the Thumbnail property will be null as you have not asked EF to load the related Thumbnail entities. Also, should you try to access the Thumbnail property later you will get an exception as the MoviesContext is no longer in scope.
Once you have a list of Movie entities, you need to convert them into ViewModels. I'm assuming here that your ViewModels are effectively read-only.
public sealed class MovieViewModel
{
public MovieViewModel(Movie movie)
{
_thumbnailId = movie.ThumbnailId;
Id = movie.Id;
Name = movie.Name;
// copy other property values across
}
readonly int _thumbnailId;
public int Id { get; private set; }
public string Name { get; private set; }
// other movie properties, all with public getters and private setters
public byte[] Thumbnail { get; private set; } // Will flesh this out later!
}
Note that we're just storing the thumbnail ID here, and not populating the Thumbnail yet. I'll come to that in a bit.
Load thumbnails separately, and cache them
So, you've loaded the movies, but at the moment you haven't loaded any thumbnails. What you need is a method that will load a single Thumbnail entity from the database given its ID. I would suggest combining this with a cache of some sort, so that once you've loaded a thumbnail image you keep it in memory for a while.
public sealed class ThumbnailCache
{
public ThumbnailCache(string connectionString)
{
_connectionString = connectionString;
}
readonly string _connectionString;
readonly Dictionary<int, Thumbnail> _cache = new Dictionary<int, Thumbnail>();
public Thumbnail GetThumbnail(int id)
{
Thumbnail thumbnail;
if (!_cache.TryGetValue(id, out thumbnail))
{
// Not in the cache, so load entity from database
using (var db = new MoviesContext(_connectionString))
{
thumbnail = db.Thumbnails.AsNoTracking().Find(id);
}
_cache.Add(id, thumbnail);
}
return thumbnail;
}
}
This is obviously a very basic cache: the retrieval is blocking, there is no error handling, and the thumbnails should really be removed from the cache if they haven't been retrieved for a while in order to keep memory usage down.
Going back to the ViewModel, you need to modify the constructor to take a reference to a cache instance, and also modify the Thumbnail property getter to retrieve the thumbnail from the cache:
public sealed class MovieViewModel
{
public MovieViewModel(Movie movie, ThumbnailCache thumbnailCache)
{
_thumbnailId = movie.ThumbnailId;
_thumbnailCache = thumbnailCache;
Id = movie.Id;
Name = movie.Name;
// copy other property values across
}
readonly int _thumbnailId;
readonly ThumbnailCache _thumbnailCache;
public int Id { get; private set; }
public string Name { get; private set; }
// other movie properties, all with public getters and private setters
public BitmapSource Thumbnail
{
get
{
if (_thumbnail == null)
{
byte[] image = _thumbnailCache.GetThumbnail(_thumbnailId).Image;
// Convert to BitmapImage for binding purposes
var bitmapImage = new BitmapImage();
bitmapImage.BeginInit();
bitmapImage.StreamSource = new MemoryStream(image);
bitmapImage.CreateOptions = BitmapCreateOptions.None;
bitmapImage.CacheOption = BitmapCacheOption.Default;
bitmapImage.EndInit();
_thumbnail = bitmapImage;
}
return _thumbnail;
}
}
BitmapSource _thumbnail;
}
Now the thumnail images will only be loaded when the Thumbnail property is accessed: if the image was already in the cache, it will be returned immediately, otherwise it will be loaded from the database first and then stored in the cache for future use.
Binding performance
The way that you bind your collection of MovieViewModels to the control in your view will have an impact on perceived loading time as well. What you want to do whenever possible is to delay the binding until your collection has been populated. This will be quicker than binding to an empty collection and then adding items to the collection one at a time. You may already know this but I thought I'd mention it just in case.
This MSDN page (Optimizing Performance: Data Binding) has some useful tips.
This awesome series of blog posts by Ian Griffiths (Too Much, Too Fast with WPF and Async) shows how various binding strategies can affect the load times of a bound list.
Only loading thumbnails when in view
Now for the most difficult bit! We've stopped the thumbnails from loading when the application starts, but we do need to load them at some point. The best time to load them is when they are visible in the UI. So the question becomes: how do I detect when the thumbnail is visible in the UI? This largely depends on the controls you are using in your view (the UI).
I'll assume that you are binding your collection of MovieViewModels to an ItemsControl of some type, such as a ListBox or ListView. Furthermore, I'll assume that you have some kind of DataTemplate configured (either as part of the ListBox/ListView markup, or in a ResourceDictionary somewhere) that is mapped to the MovieViewModel type. A very simple version of that DataTemplate might look like this:
<DataTemplate DataType="{x:Type ...}">
<StackPanel>
<Image Source="{Binding Thumbnail}" Stretch="Fill" Width="100" Height="100" />
<TextBlock Text="{Binding Name}" />
</StackPanel>
</DataTemplate>
If you are using a ListBox, even if you change the panel it uses to something like a WrapPanel, the ListBox's ControlTemplate contains a ScrollViewer, which provides the scroll bars and handles any scrolling. In this case then, we can say that a thumbnail is visible when it appears within the ScrollViewer's viewport. Therefore, we need a custom ScrollViewer element that, when scrolled, determines which of its "children" are visible in the viewport, and flags them accordingly. The best way of flagging them is to use an attached Boolean property: in this way, we can modify the DataTemplate to trigger on the attached property value changing and load the thumbnail at that point.
The following ScrollViewer descendant (sorry for the terrible name!) will do just that (note that this could probably be done with an attached behaviour instead of having to subclass, but this answer is long enough as it is).
public sealed class MyScrollViewer : ScrollViewer
{
public static readonly DependencyProperty IsInViewportProperty =
DependencyProperty.RegisterAttached("IsInViewport", typeof(bool), typeof(MyScrollViewer));
public static bool GetIsInViewport(UIElement element)
{
return (bool) element.GetValue(IsInViewportProperty);
}
public static void SetIsInViewport(UIElement element, bool value)
{
element.SetValue(IsInViewportProperty, value);
}
protected override void OnScrollChanged(ScrollChangedEventArgs e)
{
base.OnScrollChanged(e);
var panel = Content as Panel;
if (panel == null)
{
return;
}
Rect viewport = new Rect(new Point(0, 0), RenderSize);
foreach (UIElement child in panel.Children)
{
if (!child.IsVisible)
{
SetIsInViewport(child, false);
continue;
}
GeneralTransform transform = child.TransformToAncestor(this);
Rect childBounds = transform.TransformBounds(new Rect(new Point(0, 0), child.RenderSize));
SetIsInViewport(child, viewport.IntersectsWith(childBounds));
}
}
}
Basically this ScrollViewer assumes that it's Content is a panel, and sets the attached IsInViewport property to true for those children of the panel that lie within the viewport, ie. are visible to the user. All that remains now is to modify the XAML for the view to include this custom ScrollViewer as part of the ListBox's template:
<Window x:Class="..."
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:my="clr-namespace:...">
<Window.Resources>
<DataTemplate DataType="{x:Type my:MovieViewModel}">
<StackPanel>
<Image x:Name="Thumbnail" Stretch="Fill" Width="100" Height="100" />
<TextBlock Text="{Binding Name}" />
</StackPanel>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding Path=(my:MyScrollViewer.IsInViewport), RelativeSource={RelativeSource AncestorType={x:Type ListBoxItem}}}"
Value="True">
<Setter TargetName="Thumbnail" Property="Source" Value="{Binding Thumbnail}" />
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</Window.Resources>
<ListBox ItemsSource="{Binding Movies}">
<ListBox.Template>
<ControlTemplate>
<my:MyScrollViewer HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
<WrapPanel IsItemsHost="True" />
</my:MyScrollViewer>
</ControlTemplate>
</ListBox.Template>
</ListBox>
</Window>
Here we have a Window containing a single ListBox. We've changed the ControlTemplate of the ListBox to include the custom ScrollViewer, and inside that is the WrapPanel that will layout the items. In the window's resources we have the DataTemplate that will be used to display each MovieViewModel. This is similar to the DataTemplate introduced earlier, but note that we are no longer binding the Image's Source property in the body of the template: instead, we use a trigger based on the IsInViewport property, and set the binding when the item becomes 'visible'. This binding will cause the MovieViewModel class's Thumbnail property getter to be called, which will load the thumbnail image either from the cache or the database. Note that the binding is to the property on the parent ListBoxItem, into which the markup for the DataTemplate is injected.
The only problem with this approach is that, as the thumbnail loading is done on the UI thread, scrolling will be affected. The easiest way to fix this would be to modify the MovieViewModel Thumbnail property getter to return a "dummy" thumbnail, schedule the call to the cache on a separate thread, then get that thread to set the Thumbnail property accordingly and raise a PropertyChanged event, thus ensuring the binding mechanism picks-up the change. There are other solutions but they would raise the complexity significantly: consider what is presented here as just a possible starting point.
Whenever you request an Entity from EF, it automatically loads all scalar properties (only associations are lazy loaded). Move the Thumbnail data to it's own Entity, make it an association (aka navigation property) and take advantage of the Lazy loading.
public class Movie
{
public int Id { get; set; }
public int ThumbnailId { get; set; }
public virtual Thumbnail Thumbnail { get; set; }
public string Name { get; set; }
public double Length { get; set; }
public DateTime ReleaseDate { get; set; }
//etc...
}
public class Thumbnail
{
public int Id { get; set; }
public byte[] Data { get; set; }
}
public class MovieViewModel
{
private readonly Movie _movie;
public MovieViewModel(Movie movie)
{
_movieModel = movieModel;
}
public byte[] Thumbnail { get { return _movie.Thumbnail.Data; } }
}
Now the thumbnail data will only be loaded from the database when Thumbnail property of the ViewModel is accessed by the UI.
I was having issues with the answer when applied to a ListBox whose ItemsSource is dynamic. In that case, when the source is modified, the ScrollViewer is not necessarily, the trigger is not fired and the images are not loaded.
My main issue concerns the lazy loading lots of highres images in a UniformGrid (which is not virtualized).
To overcome this, I applied a Behavior on ListBoxItem. I find it is a good solution too because you do not have to subclass the ScrollViewer and change the ListBox's Template but only ListBoxItem's.
Add a behavior to your project :
namespace behaviors
{
using System.Windows;
using System.Windows.Controls;
using System.Windows.Interactivity;
using System.Windows.Media;
public class ListBoxItemIsVisibleBehavior : Behavior<ListBoxItem>
{
public static readonly DependencyProperty IsInViewportProperty = DependencyProperty.RegisterAttached("IsInViewport", typeof(bool), typeof(ListBoxItemIsVisibleBehavior));
public static bool GetIsInViewport(UIElement element)
{
return (bool)element.GetValue(IsInViewportProperty);
}
public static void SetIsInViewport(UIElement element, bool value)
{
element.SetValue(IsInViewportProperty, value);
}
protected override void OnAttached()
{
base.OnAttached();
try
{
this.AssociatedObject.LayoutUpdated += this.AssociatedObject_LayoutUpdated;
}
catch { }
}
protected override void OnDetaching()
{
try
{
this.AssociatedObject.LayoutUpdated -= this.AssociatedObject_LayoutUpdated;
}
catch { }
base.OnDetaching();
}
private void AssociatedObject_LayoutUpdated(object sender, System.EventArgs e)
{
if (this.AssociatedObject.IsVisible == false)
{
SetIsInViewport(this.AssociatedObject, false);
return;
}
var container = WpfExtensions.FindParent<ListBox>(this.AssociatedObject);
if (container == null)
{
return;
}
var visible = this.IsVisibleToUser(this.AssociatedObject, container) == true;
SetIsInViewport(this.AssociatedObject, visible);
}
private bool IsVisibleToUser(FrameworkElement element, FrameworkElement container)
{
if (element.IsVisible == false)
{
return false;
}
GeneralTransform transform = element.TransformToAncestor(container);
Rect bounds = transform.TransformBounds(new Rect(0.0, 0.0, element.ActualWidth, element.ActualHeight));
Rect viewport = new Rect(0.0, 0.0, container.ActualWidth, container.ActualHeight);
return viewport.IntersectsWith(bounds);
}
}
}
Then you would have to use this answer in order to add a behavior to your ListBoxItem style : How to add a Blend Behavior in a Style Setter
This leads to add a helper in your project :
public class Behaviors : List<Behavior>
{
}
public static class SupplementaryInteraction
{
public static Behaviors GetBehaviors(DependencyObject obj)
{
return (Behaviors)obj.GetValue(BehaviorsProperty);
}
public static void SetBehaviors(DependencyObject obj, Behaviors value)
{
obj.SetValue(BehaviorsProperty, value);
}
public static readonly DependencyProperty BehaviorsProperty =
DependencyProperty.RegisterAttached("Behaviors", typeof(Behaviors), typeof(SupplementaryInteraction), new UIPropertyMetadata(null, OnPropertyBehaviorsChanged));
private static void OnPropertyBehaviorsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var behaviors = Interaction.GetBehaviors(d);
foreach (var behavior in e.NewValue as Behaviors) behaviors.Add(behavior);
}
}
Then add the behavior in a resource somewhere in your control :
<UserControl.Resources>
<behaviors:Behaviors x:Key="behaviors" x:Shared="False">
<behaviors:ListBoxItemIsVisibleBehavior />
</behaviors:Behaviors>
</UserControl.Resources>
And a reference of this resource and a trigger to the style of your ListBoxItem :
<Style x:Key="_ListBoxItemStyle" TargetType="ListBoxItem">
<Setter Property="behaviors:SupplementaryInteraction.Behaviors" Value="{StaticResource behaviors}"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListBoxItem">
<StackPanel d:DataContext="{d:DesignInstance my:MovieViewModel}">
<Image x:Name="Thumbnail" Stretch="Fill" Width="100" Height="100" />
<TextBlock Text="{Binding Name}" />
</StackPanel>
<ControlTemplate.Triggers>
<DataTrigger Binding="{Binding Path=(behaviors:ListBoxItemIsVisibleBehavior.IsInViewport), RelativeSource={RelativeSource Self}}"
Value="True">
<Setter TargetName="Thumbnail"
Property="Source"
Value="{Binding Thumbnail}" />
</DataTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
And reference the style in your ListBox :
<ListBox ItemsSource="{Binding Movies}"
Style="{StaticResource _ListBoxItemStyle}">
</ListBox>
In the case of a UniformGrid :
<ListBox ItemsSource="{Binding Movies}"
Style="{StaticResource _ListBoxItemStyle}">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Columns="5" />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
</ListBox>
Do you want to load all the images at once? (I recon not all 4000+ movie thumbnails will be shown on screen at the same time). The easiest way I think you could achieve this is to load the images when needed (e.g. only load the ones that are showing and dispose of these (to conserve memory) when not showing).
This should speed up things since you only have to instantiate the objects (and let the ObservableCollection points to the memory address of the objects) and again only load images when needed.
hints to an answer are:
Divide the screen in blocks (pages)
And upon changing the index load the new images (you've already got an observable collection list)
If you still run into difficulties I'll try to give a more clear answer :)
Goodluck
It's taking that long because it's putting everything you've loaded into EF's Change Tracker. That's all 4000 records being tracked in memory, which is understandably causing your app to slow down. If you're not actually doing any editing on the page, I suggest that you use .AsNoTracking() when you grab the Movies.
As so:
var allMovies = MyDbContext.Movies.AsNoTracking();
foreach (var movie in allMovies)
MovieVms.Add(new MovieViewModel(movie));
MSDN link on it can be found here.
im new to WPF and im using this code to fill a uniformgrid with grids,
public MainWindow()
{
InitializeComponent();
SolidColorBrush defaultBrush = new SolidColorBrush(Colors.Wheat);
SolidColorBrush alternateBrush = new SolidColorBrush(Colors.Black);
Char L = 'A';
int N = 1;
for (int i = 0; i < 64; i++)
{
Grid cell = new Grid();
if(N==9)
{
N=1;
L++;
}
if ((i + i / 8) % 2 == 0)
{
cell.Name = L + N.ToString();
cell.Background = defaultBrush;
ChessBoard.Children.Add(cell);
}
else
{
cell.Name = L + N.ToString();
cell.Background = alternateBrush;
ChessBoard.Children.Add(cell);
}
N++
}
im then trying to find out what name a certain grid has when i click down on the uniformedgrid called ChessBoard.
private void ChessBoard_MouseLeftButtonDown(object sender, MouseButtonEventArgs args)
{
var element = (UIElement)args.Source;
element.Opacity = 0.5;
}
the opacity line is test to make sure im on the right grid, and it works and changs the Opacity of the grid im clicking.
What i need help with is to find the Name attribute of the element.
Ok, delete all your code and start all over.
If you're working with WPF, you really need to understand and embrace The WPF Mentality.
As a general rule, creating or manipulating UI elements in procedural code is discouraged.
Instead, you create a proper ViewModel which will hold all the relevant properties that represent the state and data shown by the UI, and use DataBinding to reflect these properties.
This is how you do what you're trying here, in WPF:
<Window x:Class="ChessBoardSample.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="350">
<ItemsControl ItemsSource="{Binding Squares}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Rows="8" Columns="8"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button x:Name="Square"
Command="{Binding DataContext.SquareClickCommand, RelativeSource={RelativeSource AncestorType=ItemsControl}}"
CommandParameter="{Binding}">
<Button.Template>
<ControlTemplate TargetType="Button">
<Grid Background="{TemplateBinding Background}"/>
</ControlTemplate>
</Button.Template>
</Button>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding IsBlack}" Value="True">
<Setter TargetName="Square" Property="Background" Value="Black"/>
</DataTrigger>
<DataTrigger Binding="{Binding IsBlack}" Value="False">
<Setter TargetName="Square" Property="Background" Value="Wheat"/>
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Window>
Code Behind:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new ChessBoard();
}
}
ViewModel:
public class ChessBoard
{
public List<ChessSquare> Squares { get; private set; }
public Command<ChessSquare> SquareClickCommand { get; private set; }
public ChessBoard()
{
Squares = new List<ChessSquare>();
for (int i = 0; i < 8; i++)
{
for (int j = 0; j < 8; j++)
{
Squares.Add(new ChessSquare() {Row = i, Column = j});
}
}
SquareClickCommand = new Command<ChessSquare>(OnSquareClick);
}
private void OnSquareClick(ChessSquare square)
{
MessageBox.Show("You clicked on Row: " + square.Row + " - Column: " + square.Column);
}
}
Data Item:
public class ChessSquare
{
public int Row { get; set; }
public int Column { get; set; }
public bool IsBlack { get { return (Row + Column) %2 == 1; }}
}
Command class (MVVM helper class):
public class Command<T>: ICommand
{
public Action<T> Action { get; set; }
public void Execute(object parameter)
{
if (Action != null && parameter is T)
Action((T)parameter);
}
public bool CanExecute(object parameter)
{
return IsEnabled;
}
private bool _isEnabled = true;
public bool IsEnabled
{
get { return _isEnabled; }
set
{
_isEnabled = value;
if (CanExecuteChanged != null)
CanExecuteChanged(this, EventArgs.Empty);
}
}
public event EventHandler CanExecuteChanged;
public Command(Action<T> action)
{
Action = action;
}
}
Result:
While all this might look like "too much code", If you look closely you'll realize that most of it is actually reusable, clean code with no dependencies on the UI, and I'm also giving you a lot of reusable infrastructure (such as the Command class).
I'm using an ItemsControl bound to a collection of Data Items, rather than creating the UI elements procedurally. This is "letting WPF do it's job".
The main idea is to Separate UI and Logic, thus gaining a LOT of scalability and maintainability.
Notice how the code behind is reduced to a mere DataContext = .... Code behind in WPF is reserved for UI Specific code, rather than code related to data or logic.
I'm using a DelegateCommand to handle the Click logic, rather than an event handler. This is useful in situations where UI elements are being created dynamically by an ItemsControl and the like.
Also notice how the OnSquareClick() method is operating against your own simple, stateful data item (ChessSquare) rather than complex arcane WPF UI objects. That's how you program in WPF. You operate against your data, not the UI.
Also notice the usage of a DataTrigger against the IsBlack property, to dynamically change the background color.
WPF Rocks. Just copy and paste my code in a File -> New Project -> WPF Application and see the results for youself.
I strongly suggest you read all the linked material. Let me know if you need further help.
All, I have a custom DataGridView control which overrides the DataGidView's OnItemsSourceChanged event. Inside this event I need to get a reference to a data set in the relevant ViewModel. Code is
public class ResourceDataGrid : DataGrid
{
protected override void OnItemsSourceChanged(
System.Collections.IEnumerable oldValue,
System.Collections.IEnumerable newValue)
{
if (Equals(newValue, oldValue))
return;
base.OnItemsSourceChanged(oldValue, newValue);
ResourceCore.ResourceManager manager = ResourceCore.ResourceManager.Instance();
ResourceDataViewModel resourceDataViewModel = ?? // How do I get my ResourceDataViewModel
List<string> l = manger.GetDataFor(resourceDataViewModel);
...
}
}
On the marked line I want to know how to get a reference to ResourceDataViewModel resourceDataViewModel. The reson is that i have multiple tabs each tab contains a data grid and ascociated ViewModel, the ViewModel holds some data that I need to retrieve [via the ResourceManager] (or is there another, better way?).
The question is, from the above event, how can I get the ascociated ResourceDataViewModel?
Thanks for your time.
Get the DataContext and cast it to the view-model type:
var viewModel = this.DataContext as ResourceDataViewModel
Put a static reference to it on your app, when the VM is created place its reference on the static and access it as needed.
You ask if there is a better way... In my experience if you find yourself subclassing a UI element in WPF there ususally is.
You can get away from embedding business logic (the choice of which data to display in the grid), by databinding your entire tab control to a view model.
To demonstrate - here is a very simple example. This is my XAML for the window hosting the tab control:
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid>
<TabControl ItemsSource="{Binding Tabs}" SelectedItem="{Binding SelectedTab}">
<TabControl.ItemContainerStyle>
<Style TargetType="TabItem">
<Setter Property="Header" Value="{Binding TabName}"></Setter>
</Style>
</TabControl.ItemContainerStyle>
<TabControl.ContentTemplate>
<DataTemplate>
<Grid>
<DataGrid ItemsSource="{Binding TabData}"></DataGrid>
</Grid>
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
</Grid>
</Window>
The data context of my window is a TabsViewModel (I am using the NotificationObject that can be found in the PRISM NuGet Package):
public class TabsViewModel: NotificationObject
{
public TabsViewModel()
{
Tabs = new[]
{
new TabViewModel("TAB1", "Data 1 Tab 1", "Data 2 Tab1"),
new TabViewModel("TAB2", "Data 1 Tab 2", "Data 2 Tab2"),
};
}
private TabViewModel _selectedTab;
public TabViewModel SelectedTab
{
get { return _selectedTab; }
set
{
if (Equals(value, _selectedTab)) return;
_selectedTab = value;
RaisePropertyChanged(() => SelectedTab);
}
}
public IEnumerable<TabViewModel> Tabs { get; set; }
}
public class TabViewModel
{
public TabViewModel(string tabName, params string[] data)
{
TabName = tabName;
TabData = data.Select(d => new RowData(){Property1 = d}).ToArray();
}
public string TabName { get; set; }
public RowData[] TabData { get; set; }
}
public class RowData
{
public string Property1 { get; set; }
}
This is obviously an over simplified case, but it means that if there is any business logic about precisely what data to show in each tab, this can reside in one of the view models, as opposed to the code behind. This gives you all the 'separation of concerns' benefits that MVVM is designed to encourage...