Reputation: 6157
I searched online for the mvvm setup and noticed when i created my sample that the tabs do not appear to generate when the application is launched. However I'm not entirely clear on why that is. Aside from the tabs not being created I have a few other questions...
I found bits a pieces of examples of this stuff online, but many were either complex or not complete. I appreciate the help, thank you.
The MainWindow.cs
using System.Collections.ObjectModel;
using System.Windows;
namespace listBinding
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
this.DataContext = new ViewModel();
InitializeComponent();
}
public class ViewModel
{
public ObservableCollection<TabItem> Tabs { get; set; }
public ViewModel()
{
Tabs = new ObservableCollection<TabItem>();
Tabs.Add(new TabItem { Header = "One", Content = "One's content" });
Tabs.Add(new TabItem { Header = "Two", Content = "Two's content" });
Tabs.Add(new TabItem { Header = "Three", Content = "Three's content" });
}
}
public class TabItem
{
public string Header { get; set; }
public string Content { get; set; }
}
private void AddItem(object sender, RoutedEventArgs e)
{
// Adds new item and generates a new tab
}
private void DeleteItem(object sender, RoutedEventArgs e)
{
// Deletes the selected tab
}
}
}
The MainWindow.xaml
<Window x:Class="listBinding.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow"
Width="525"
Height="350">
<DockPanel>
<Menu DockPanel.Dock="Top">
<MenuItem Header="_Add" Click="AddItem"></MenuItem>
<MenuItem Header="_Delete" Click="DeleteItem"></MenuItem>
</Menu>
<TabControl ItemsSource="{Binding Tabs}">
<TabControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Header}" />
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate>
<DataTemplate>
<TextBlock
Text="{Binding Content}" />
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
</DockPanel>
</Window>
Upvotes: 1
Views: 273
Reputation: 9394
Ok. First of all don't put your DataContext in the Code-Behind of your view.
I would suggest you to create a small folder-hierarchy in your solution like:
ViewModel
In this folder there are your classes where your logic is contained. A viewmodel know's nothing about any view-object (xaml-file).
In your case I would create a class called MainWindowViewModel
which looks like:
internal class MainWindowViewModel : INotifyPropertyChanged
{
private ICommand addCommand;
private ObservableCollection<ContentItem> contentItems;
private ICommand deleteCommand;
private ContentItem selectedContentItem;
public MainWindowViewModel()
{
ContentItems.Add(new ContentItem("One", "One's content"));
ContentItems.Add(new ContentItem("Two", "Two's content"));
ContentItems.Add(new ContentItem("Three", "Three's content"));
}
public ObservableCollection<ContentItem> ContentItems
{
get { return contentItems ?? (contentItems = new ObservableCollection<ContentItem>()); }
}
public ICommand AddCommand
{
get { return addCommand ?? (addCommand = new RelayCommand(AddContentItem)); }
}
public ICommand DeleteCommand
{
get { return deleteCommand ?? (deleteCommand = new RelayCommand(DeleteContentItem, CanDeleteContentItem)); }
}
public ContentItem SelectedContentItem
{
get { return selectedContentItem; }
set
{
selectedContentItem = value;
OnPropertyChanged();
}
}
private bool CanDeleteContentItem(object parameter)
{
return SelectedContentItem != null;
}
private void DeleteContentItem(object parameter)
{
ContentItems.Remove(SelectedContentItem);
}
private void AddContentItem(object parameter)
{
ContentItems.Add(new ContentItem("New content item", DateTime.Now.ToLongDateString()));
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
}
The ContentItems-Collection contains all your items you want to display as TabItems in the view. The SelectedContentItem-Property always contains the currently selected TabItem in the TabControl.
The Commands AddCommand
and DeleteCommand
are the Commands which are executed if you click on Add or Delete. In MVVM you usually don't use events for the communication between View and ViewModel.
Helper
In this folder I've put a class called RelayCommand
which I already used in the MainWindowViewModel. The class looks like this:
public class RelayCommand : ICommand
{
private readonly Predicate<object> canExecute;
private readonly Action<object> execute;
public RelayCommand(Action<object> execute, Predicate<object> canExecute = null)
{
if (execute == null)
throw new ArgumentNullException("execute");
this.execute = execute;
this.canExecute = canExecute;
}
public bool CanExecute(object parameter)
{
return canExecute == null || canExecute(parameter);
}
public void Execute(object parameter)
{
execute(parameter);
}
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
}
You need this class (or a similar implementation of ICommand) to do the interaction between objects in your view where you want to click (MenuItem's, Buttons, ...) and the corresponding ViewModel.
Model
Here are your DataObjects. In this case it's the ContentItem with it's two properties.
public class ContentItem : INotifyPropertyChanged
{
private string contentText;
private string header;
public ContentItem(string header, string contentText)
{
Header = header;
ContentText = contentText;
}
public string Header
{
get { return header; }
set
{
header = value;
OnPropertyChanged();
}
}
public string ContentText
{
get { return contentText; }
set
{
contentText = value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
}
View
In this folder is what the user of your application got to see. In your case there is just on file called MainWindowView.xaml which looks like:
<Window x:Class="MVVMDemo.View.MainWindowView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindowView" Height="350" Width="525"
WindowStartupLocation="CenterScreen">
<DockPanel>
<Menu DockPanel.Dock="Top">
<MenuItem Header="_Add" Command="{Binding AddCommand}"/>
<MenuItem Header="_Delete" Command="{Binding DeleteCommand}"/>
</Menu>
<TabControl ItemsSource="{Binding ContentItems}"
SelectedItem="{Binding SelectedContentItem, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
<TabControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Header}"/>
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate>
<DataTemplate>
<TextBlock Text="{Binding ContentText}"/>
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
</DockPanel>
</Window>
As you can see the MenuItems are bound to the ICommand-Properties in the ViewModel. So you don't need an Event here for the communication. And because your TabControl is bound to a collection of model-objects in your viewmodel you can add or delete items in the tabcontrol just be add or remove the model-objects from the bound collection.
Unti now, there is no connection between the View and the ViewModel. If you run the code now, there will be no entries in the TabControl.
There are several ways to do the connection between the view and the viewmodel.
In the App.xaml / App.xaml.cs:
Open the App.xaml and remove the part StartupUri="MainWindow.xaml"
and replace it with Startup="App_OnStartup"
. Now you have to create the event-handler for App_OnStartup in the App.xaml.cs which looks like:
private void App_OnStartup(object sender, StartupEventArgs e)
{
MainWindowViewModel viewModel = new MainWindowViewModel();
MainWindowView view = new MainWindowView
{
DataContext = viewModel
};
view.ShowDialog();
}
And now you have the connection.
In the XAML of your MainWindowView
Another way to connect the view to the viewmodel is to set the datacontext of the view directly in the xaml.
To do this you have to add an xmlns-to your MainWindowViewModel which looks like: xmlns:viewModel="clr-namespace:MVVMDemo.ViewModel"
and then you can add the following xaml right after the Window-Tag:
<Window.DataContext>
<viewModel:MainWindowViewModel/>
</Window.DataContext>
I hope this sample helps you. If you have any questions about it, feel free to ask
Upvotes: 3
Reputation: 10784
Simply change
this.DataContext = this;
to
this.DataContext = new ViewModel();
If you watch the Visual Studio output window, you'll often see these types of errors readily when you run your program. For example, with your original code, you would see
"System.Windows.Data Error: 40 : BindingExpression path error: 'Tabs' property not found on 'object' ''MainWindow' (Name='')'. BindingExpression:Path=Tabs; DataItem="
You may also find this WPF DataBinding Cheatsheet helpful.
For many WPF/mvvm applications, I have found that the MVVM Light Toolkit is a helpful library.
You can find my notes on the general pattern and some examples using the toolkit at http://davisnw.github.io/mvvm-palindrome/Introduction/ (the example code probably could do with some updates, but the basics should still be relevant).
Upvotes: 1
Reputation: 3018
Before you go through the answers, you need to get an idea of MVVM: Basic MVVM and ICommand Usage Example
To answer your questions:
1. a. DataContext to the view should be the ViewModel.
this.DataContext = new ViewModel();
b. ViewModel should always implement INotifyPropertyChanged
.So, currently when through code if you are initialising tabs,it won't be shown to the screen since no notification & also wrong data context.
You need to use command binding for Add button, that should be binded with ViewModel's AddCommand
property(ICommand type) & then you attach the AddItem function to the command(using constructor).Add new TabItem to Tabs list & it will automatically get reflected to the screen since it is observable collection & implements INPC.
You can do it in two ways: using Converter on Delete button's Visibity or CanExecute of DeleteCommand.
Make the DeleteCommand
point to DeleteItem() in ViewModel.
Upvotes: 1
Reputation: 29036
You are assigning the Datacontext for the MainWindow()
as MainWindow()
itself, by setting this.DataContext = this;
so it will not bind the content from the view model. so you have to assign viewmodel as dataContext for the MainWindow()
. make the following Change
Replace
public MainWindow()
{
this.DataContext = this;
InitializeComponent();
}
With
public MainWindow()
{
this.DataContext = new ViewModel();
InitializeComponent();
}
Upvotes: 0