JokerMartini
JokerMartini

Reputation: 6157

WPF Data Binding is not populating tabs?

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...

  1. [DONE] Why do the tabs not appear, even after is set up the binding? (Main question)
  2. When the user clicks 'Add' how can it add another item to the data?
  3. How can i make it so when only a tab is selected the 'Delete Button is enabled'?
  4. When the user clicks 'Delete' it deletes the selected tab if there is a tab selected.

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

Answers (4)

Tomtom
Tomtom

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:

  • Converter
  • Helper
  • Model
  • View
  • ViewModel

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

Nathan
Nathan

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

Nikita Shrivastava
Nikita Shrivastava

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();
  1. 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.

  2. 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.

  3. You can do it in two ways: using Converter on Delete button's Visibity or CanExecute of DeleteCommand.

  4. Make the DeleteCommand point to DeleteItem() in ViewModel.

Upvotes: 1

sujith karivelil
sujith karivelil

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

Related Questions