Search code examples
xamlmvvmwin-universal-appmvvm-lightwindows-8.1-universal

Universal Windows Apps 8.1 data binding issue


Simple two way data binding to a model's property is not working, to reproduce the issue, I have created a new project in Visual Studio 2013 i.e. with Blank App (Universal Apps) template with .NET framework 4.5

Project folders and files

The model

namespace UWP.MVVM.Models
{
    public class PersonModel
    {
        public int Id { get; set; }

        public string FirstName { get; set; }

        public string LastName { get; set; }
    }
}

The base view model

namespace UWP.MVVM.Core
{
    using System.ComponentModel;
    using System.Runtime.CompilerServices;

    public class VMBase : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        public void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
        {
            if (this.PropertyChanged != null)
            {
                this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }
    }
}

The INavigable interface

namespace UWP.MVVM.Core
{
#if WINDOWS_PHONE_APP
    using Windows.Phone.UI.Input;
#endif

    public interface INavigable
    {
        void Activate(object parameter);

        void Deactivate(object parameter);

#if WINDOWS_PHONE_APP
        void BackButtonPressed(BackPressedEventArgs e);
#endif
    }
}

The main view model

namespace UWP.MVVM.ViewModels
{
    using UWP.MVVM.Core;
    using UWP.MVVM.Models;
#if WINDOWS_PHONE_APP
    using Windows.Phone.UI.Input;
#endif

    public class MainViewModel : VMBase, INavigable
    {
        private PersonModel person;

        public MainViewModel()
        {
            this.Person = new PersonModel();
        }

        public PersonModel Person
        {
            get
            {
                return this.person;
            }
            set
            {
                if (value == this.person)
                {
                    return;
                }

                this.person = value;
                this.NotifyPropertyChanged();
            }
        }

        public void Activate(object parameter)
        {
            this.Person.FirstName = "Gerrard";
        }

        public void Deactivate(object parameter)
        {
        }

#if WINDOWS_PHONE_APP
        public void BackButtonPressed(BackPressedEventArgs e)
        {
        }
#endif
    }
}

The main page view

<Page
    x:Class="UWP.MVVM.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:UWP.MVVM"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:vm="using:UWP.MVVM.ViewModels"
    mc:Ignorable="d"
    Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
    <!--<Page.DataContext>
        <vm:MainViewModel/>
    </Page.DataContext>-->

    <Grid Margin="24,24">
        <TextBox Header="First Name" 
                 Text="{Binding Person.FirstName}"/>
    </Grid>
</Page>

The main page code behind

namespace UWP.MVVM
{
    using UWP.MVVM.Core;
#if WINDOWS_PHONE_APP
    using Windows.Phone.UI.Input;
#endif
    using Windows.UI.Xaml.Controls;
    using Windows.UI.Xaml.Navigation;
    using UWP.MVVM.ViewModels;

    /// <summary>
    /// An empty page that can be used on its own or navigated to within a Frame.
    /// </summary>
    public sealed partial class MainPage : Page
    {
        public MainPage()
        {
            this.InitializeComponent();

            this.NavigationCacheMode = NavigationCacheMode.Required;
            this.DataContext = new MainViewModel();
        }

        /// <summary>
        /// Invoked when this page is about to be displayed in a Frame.
        /// </summary>
        /// <param name="e">Event data that describes how this page was reached.
        /// This parameter is typically used to configure the page.</param>
        protected override void OnNavigatedTo(NavigationEventArgs e)
        {
            base.OnNavigatedTo(e);
            var navigableViewModel = this.DataContext as INavigable;
            if (navigableViewModel != null)
            {
                navigableViewModel.Activate(e.Parameter);
            }

#if WINDOWS_PHONE_APP
            HardwareButtons.BackPressed += HardwareButtons_BackPressed;
#endif
        }

        protected override void OnNavigatedFrom(NavigationEventArgs e)
        {
            base.OnNavigatedFrom(e);
            var navigableViewModel = this.DataContext as INavigable;
            if (navigableViewModel != null)
            {
                navigableViewModel.Deactivate(e.Parameter);
            }

#if WINDOWS_PHONE_APP
            HardwareButtons.BackPressed -= HardwareButtons_BackPressed;
#endif
        }

#if WINDOWS_PHONE_APP
        private void HardwareButtons_BackPressed(object sender, BackPressedEventArgs e)
        {
            var navigableViewModel = this.DataContext as INavigable;
            if (navigableViewModel != null)
            {
                navigableViewModel.BackButtonPressed(e);
            }
        }
#endif
    }
}

I tried using Mode=TwoWay on the TextBox and it is not working, but when I set the DataContext in xaml instead of the code behind then data binding works even without the Mode=TwoWay property.

I want to set the DataContext in the code behind file as in the real project where I am having this issue, I am using MVVM-light libraries along with its SimpleIoc container, so I want to get the view model instance from SimpleIoc and set the DataContext because the view model dependencies are injected by the SimpleIoc and the code will be a lot cleaner.


Solution

  • The problem is: you only notify the change of PersonModel Person. The ViewModel need to notify the change of the property of PersonModel.

    Since you are using MVVM Light, change your Model to:

    public class PersonModel : ObservableObject
    {
        public int Id { get; set; }
    
        string _FirstName = "";
        public string FirstName {
            get {
                return _FirstName;
            }
            set {
                Set(ref _FirstName, value);
            }
        }
    
        public string LastName { get; set; }
    }