0
votes

i'm learning the MVVM Pattern and have some trouble with Databindings. I understand how it works, but in my example it's using the wrong DataContext to search for the binding.

I did not found another Question that suits to my problem.

So i have this View:

<UserControl x:Class="Kenshinaro.CashRegister.UI.View.CashRegister.CashRegisterChoiceView"
             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" 
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800">
    <ItemsControl ItemsSource="{Binding CashRegisters}">
        <ItemsControl.ItemsPanel>
            <ItemsPanelTemplate>
                <WrapPanel></WrapPanel>
            </ItemsPanelTemplate>
        </ItemsControl.ItemsPanel>
    </ItemsControl>
</UserControl>

With following ViewModel:

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Kenshinaro.CashRegister.UI.View.CashRegister;

namespace Kenshinaro.CashRegister.UI.ViewModel.CashRegister
{
    public class CashRegisterChoiceViewModel : MVVM.ViewModel
    {
        private ObservableCollection<CashRegisterItemsControlView> _cashRegisters = new ObservableCollection<CashRegisterItemsControlView>()
        {
            new CashRegisterItemsControlView()
            {
                DataContext = new CashRegisterItemsControlViewModel()
                {
                    View = new CashRegisterCardView()
                    {
                        DataContext = new CashRegisterCardViewModel()
                        {
                            Model = new Model.CashRegister()
                            {
                                Name = "1"
                            }
                        }
                    }
                }
            },
            new CashRegisterItemsControlView()
            {
                DataContext = new CashRegisterItemsControlViewModel()
                {
                    View = new CashRegisterCardView()
                    {
                        DataContext = new CashRegisterCardViewModel()
                        {
                            Model = new Model.CashRegister()
                            {
                                Name = "2"
                            }
                        }
                    }
                }
            },
            new CashRegisterItemsControlView()
            {
                DataContext = new CashRegisterItemsControlViewModel()
                {
                    View = new CashRegisterCardView()
                    {
                        DataContext = new CashRegisterCardViewModel()
                        {
                            Model = new Model.CashRegister()
                            {
                                Name = "3"
                            }
                        }
                    }
                }
            }
        };

        public ObservableCollection<CashRegisterItemsControlView> CashRegisters
        {
            get => _cashRegisters;
            set => SetProperty(ref _cashRegisters, value);
        }
    }
}

(My Base ViewModel class is implementing the INotifyPropertyChanged interface)

So this View shows a list of this view:

<UserControl x:Class="Kenshinaro.CashRegister.UI.View.CashRegister.CashRegisterItemsControlView"
             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:Kenshinaro.CashRegister.UI.View.CashRegister"
             xmlns:md="http://materialdesigninxaml.net/winfx/xaml/themes"
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800">
    <md:Card Margin="10">
        <Grid >
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
            </Grid.RowDefinitions>
            <local:CashRegisterCardView Padding="5"/>
            <Button Margin="20" Grid.Row="1" Content="Pick me"/>
        </Grid>
    </md:Card>
</UserControl>

ViewModel:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Kenshinaro.CashRegister.UI.ViewModel.CashRegister
{
    public class CashRegisterItemsControlViewModel : MVVM.ViewModel
    {
        private View.CashRegister.CashRegisterCardView _view;

        public View.CashRegister.CashRegisterCardView View
        {
            get => _view;
            set => SetProperty(ref _view, value);
        }
    }
}

And this View shows also this view:

<UserControl x:Class="Kenshinaro.CashRegister.UI.View.CashRegister.CashRegisterCardView"
             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:Kenshinaro.CashRegister.UI.View.CashRegister"
             xmlns:md="http://materialdesigninxaml.net/winfx/xaml/themes"
             xmlns:diag="clr-namespace:System.Diagnostics;assembly=WindowsBase"
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800" Background="White" >
    <Grid>
        <TextBlock Text="{Binding DataContext.Name, RelativeSource={RelativeSource AncestorType=local:CashRegisterCardView}}"/>
    </Grid>
</UserControl>

ViewModel:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Kenshinaro.CashRegister.Model;

namespace Kenshinaro.CashRegister.UI.ViewModel.CashRegister
{
    public class CashRegisterCardViewModel : MVVM.ViewModel
    {
        private Model.CashRegister _model;

        public Model.CashRegister Model
        {
            get => _model;
            set => SetProperty(ref _model, value);
        }

        public String Name
        {
            get => _model.Name;
            set => _model.Name = value;
        }
    }
}

Now if I start the App the Binding on the TextBlock in the last view doesn't appear. The Output of VisualStudio says:

System.Windows.Data Error: 40 : BindingExpression path error: 'Name' property not found on 'object' ''CashRegisterItemsControlViewModel' (HashCode=35528341)'. BindingExpression:Path=DataContext.Name; DataItem='CashRegisterCardView' (Name=''); target element is 'TextBlock' (Name=''); target property is 'Text' (type 'String')

As I can get out of it, it is searching for the Property in the DataContext of CashRegisterItemsControlView but I just don't understand why, because i set the DataContext manually on collection initialization (Just for testing):

private ObservableCollection<CashRegisterItemsControlView> _cashRegisters = new ObservableCollection<CashRegisterItemsControlView>()
        {
            new CashRegisterItemsControlView()
            {
                DataContext = new CashRegisterItemsControlViewModel()
                {
                    View = new CashRegisterCardView()
                    {
                        DataContext = new CashRegisterCardViewModel()
                        {
                            Model = new Model.CashRegister()
                            {
                                Name = "1"
                            }
                        }
                    }
                }
            },
...

So why it's still taking the wrong DataContext?

Now when i change the Binding to View.DataContext.Name it's searching in the manually set DataContext:

System.Windows.Data Error: 40 : BindingExpression path error: 'View' property not found on 'object' ''CashRegisterCardView' (Name='')'. BindingExpression:Path=View.DataContext.Name; DataItem='CashRegisterCardView' (Name=''); target element is 'TextBlock' (Name=''); target property is 'Text' (type 'String')

I'm really confused.. I hope you can help me.

Thanks!

If you need information about the other classes just ask. I don't want to put more code into the question as it just long enough.

1
First of all, never create view elements in a view model. That's not what MVVM is about. In general, declare DataTemplates for certain view model types. Then the view elements would be generated automatically by these templates. Start reading here: Data Templating Overview. - Clemens
@Clemens As I mentioned the collection Initialization is just for testing. It will be complete reworked when the problem with the binding is solved. I was testing MVVM with ItemsControl. But thanks for your advice. - Kenshinaro
As a general rule, never explicitly set any DataContext, except that of a Window or Page. For ItemsControls specifically, the DataContext of the individual item containers is automatically set (by the framework) to the appropriate item of the ItemsSource collection. - Clemens

1 Answers

1
votes

View models have references to views. This is a violation of MVVM pattern.

The root of problem is in the CashRegisterItemsControlView implementation:

<UserControl x:Class="Kenshinaro.CashRegister.UI.View.CashRegister.CashRegisterItemsControlView"
             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:Kenshinaro.CashRegister.UI.View.CashRegister"
             xmlns:md="http://materialdesigninxaml.net/winfx/xaml/themes"
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800">
    <md:Card Margin="10">
        <Grid >
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
            </Grid.RowDefinitions>
            <local:CashRegisterCardView Padding="5"/>
            <Button Margin="20" Grid.Row="1" Content="Pick me"/>
        </Grid>
    </md:Card>
</UserControl>

it creates an instance of CashRegisterCardView (<local:CashRegisterCardView Padding="5"/>) which doesn't have its own DataContext, and inherits DataContext from parent. View from CashRegisterItemsControlViewModel is not used.

You can change it to make it work, like this:

<UserControl x:Class="Kenshinaro.CashRegister.UI.View.CashRegister.CashRegisterItemsControlView"
             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:Kenshinaro.CashRegister.UI.View.CashRegister"
             xmlns:md="http://materialdesigninxaml.net/winfx/xaml/themes"
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800">
    <md:Card Margin="10">
        <Grid >
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
            </Grid.RowDefinitions>
            <ContentControl Content="{Binding View}" Padding="5"/>
            <Button Margin="20" Grid.Row="1" Content="Pick me"/>
        </Grid>
    </md:Card>
</UserControl>

but it would be much better to rework your app architecture