How to set data context of ViewModela View's xaml?

970 views Asked by At

I'm trying to set the data context of a View to the list contained in it's ViewModel. But when I've tested the current set up, it seems the data context between the ViewModel and View is set incorrectly.

To debug this issue I set up a message box in the constructor of my View, and I get the following error message, which hints at the data context not being set correctly: "Object reference not set to an instance of an object"

The list is also used in another ViewModel which shows that the list is not empty, which hints further at a data context issue.

Does anyone know what the flaw is in setting up the data context between the View and ViewModel?

This is the ViewModel containing the list:

namespace LC_Points.ViewModel
{
    public class ViewSubjectGradeViewModel 
    {


        public ViewSubjectGradeViewModel()
        {

            AddedSubjectGradePairs = new ObservableCollection<ScoreModel>();

        }


        public ObservableCollection<ScoreModel> AddedSubjectGradePairs { get; set; }       

    }
}

And this is the View and View code behind:

<Page x:Class="LC_Points.View.ViewSubjectGradePage"
      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:local="using:LC_Points.View"
      xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
      xmlns:vms="using:LC_Points.ViewModel"
      Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
      DataContext="{Binding ViewSubjectGradeViewModelProperty1}"
      mc:Ignorable="d">



    <Grid x:Name="LayoutRoot">

        <Grid.ChildrenTransitions>
            <TransitionCollection>
                <EntranceThemeTransition />
            </TransitionCollection>
        </Grid.ChildrenTransitions>

        <Grid.RowDefinitions>
            <RowDefinition Height="40*" />
            <RowDefinition Height="20*" />
            <RowDefinition Height="30*" />
            <RowDefinition Height="30*" />
            <RowDefinition Height="20*" />
            <RowDefinition Height="20*" />
        </Grid.RowDefinitions>

        <!--  Title Panel  -->
        <StackPanel Grid.Row="0" Margin="19,0,0,0">
            <TextBlock Margin="0,12,0,0"
                       Style="{ThemeResource TitleTextBlockStyle}"
                       Text="LC POINTS" />
            <TextBlock Margin="0,-6.5,0,26.5"
                       CharacterSpacing="{ThemeResource PivotHeaderItemCharacterSpacing}"
                       Foreground="DarkGreen"
                       Style="{ThemeResource HeaderTextBlockStyle}"
                       Text="View Grades" />
        </StackPanel>

        <!--  TODO: Content should be placed within the following grid  -->
        <Grid x:Name="ContentRoot"
              Grid.Row="1"
              Margin="19,9.5,19,0">

            <ListBox Height="400"
                     Margin="0,0,0,-329"
                     VerticalAlignment="Top"
                     ItemsSource="{Binding AddedSubjectGradePairs}">
                <ListBox.ItemTemplate>
                    <DataTemplate>
                        <TextBlock>
                            <Run Text="{Binding Subject}" /><Run Text=" - " /><Run Text="{Binding Points}" />
                        </TextBlock>
                    </DataTemplate>
                </ListBox.ItemTemplate>
            </ListBox>

        </Grid>
    </Grid>
</Page>

View code behind:

namespace LC_Points.View
{
    /// <summary>
    /// An empty page that can be used on its own or navigated to within a Frame.
    /// </summary>
    public sealed partial class ViewSubjectGradePage : Page
    {
        private NavigationHelper navigationHelper;
        private ObservableDictionary defaultViewModel = new ObservableDictionary();


        public ViewSubjectGradePage()
        {
            this.InitializeComponent();

            this.navigationHelper = new NavigationHelper(this);
            this.navigationHelper.LoadState += this.NavigationHelper_LoadState;
            this.navigationHelper.SaveState += this.NavigationHelper_SaveState;

            var messageDialog = new MessageDialog(DataContext.GetType().ToString());
            messageDialog.ShowAsync();
        }

        /// <summary>
        /// Gets the <see cref="NavigationHelper"/> associated with this <see cref="Page"/>.
        /// </summary>
        public NavigationHelper NavigationHelper
        {
            get { return this.navigationHelper; }
        }


        /// <summary>
        /// Gets the view model for this <see cref="Page"/>.
        /// This can be changed to a strongly typed view model.
        /// </summary>
        public ObservableDictionary DefaultViewModel
        {
            get { return this.defaultViewModel; }
        }



        private void NavigationHelper_LoadState(object sender, LoadStateEventArgs e)
        {
        }


        private void NavigationHelper_SaveState(object sender, SaveStateEventArgs e)
        {
        }

        #region NavigationHelper registration


        protected override void OnNavigatedTo(NavigationEventArgs e)
        {
            this.navigationHelper.OnNavigatedTo(e);
        }

        protected override void OnNavigatedFrom(NavigationEventArgs e)
        {
            this.navigationHelper.OnNavigatedFrom(e);
        }

        #endregion
    }
}
2

There are 2 answers

6
ManOVision On BEST ANSWER

You may want to delete any generated code from your View's code behind that the template provides that you aren't using. This can cause confusion because the template wants you to use a local ObservableCollection as your DataContext.

There are 3 main ways of setting up a ViewModel to the DataContext of a View.

  1. Use the code behind of the View:

    public ViewSubjectGradePage()
    {
        this.InitializeComponent();
        this.DataContext = new ViewSubjectGradeViewModel();
    }
    
  2. Use XAML (other Page properties removed for easier reading):

    <Page x:Class="LC_Points.View.ViewSubjectGradePage"
        xmlns:vms="using:LC_Points.ViewModel">
        <Page.DataContext>
            <vms:ViewSubjectGradeViewModel/>
        </Page.DataContext>
    </Page>
    
  3. Use an MVVM framework like Prism. This will auto-wire your View and ViewModel based on standard naming conventions.

I prefer option 3 since it can provide a much more loosely coupled system, but may be overkill for a very small project plus there is a learning curve with any framework, but the benefits are great. Option 2 is my second vote since it keeps the code behind cleaner. Option 1 is something I don't do anymore, but is a nice quick way to get it working.

0
Johnathon Sullinger On

I believe this to be due to how you are assigning the DataContext. In your XAML, you are binding the DataContext to a property. I don't see anywhere in your code where you are actually assigning the data context to the defaultViewModel or the properties within it.

Try to update your constructor to the following.

    public ViewSubjectGradePage()
    {
        this.InitializeComponent();

        this.navigationHelper = new NavigationHelper(this);
        this.navigationHelper.LoadState += this.NavigationHelper_LoadState;
        this.navigationHelper.SaveState += this.NavigationHelper_SaveState;

        this.DataContext = this.DefaultViewModel;
    }

If you are wanting to assign it to a property within the DefaultViewModel, you may do so there as well. Then remove the DataContext assignment in your XAML.