Search code examples
uwpuno-platformwinui

Raisepropertychanged is null and does not notify the property after datacontext changed


I have a mainPage in which I am calling my user control. The idea is that I have a TextBlock, and upon click of a button I am changing the Text which is above the Hello World name.

<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <TextBlock Text="Hello, world!" Margin="20" FontSize="30" />
        <local1:GenericControl></local1:GenericControl>

        <Button x:Name="create" Click="clickBtn"/>
    </Grid>

MainPage.xaml.cs

 public sealed partial class MainPage : Page
    {
        public TestViewModel viewModel { get; set; } = new TestViewModel();

        public MainPage()
        {
            this.InitializeComponent();
            this.DataContext = viewModel;
        }

        public void clickBtn(object sender, RoutedEventArgs e) {

            var m = ((Button)sender).DataContext as TestViewModel;
            m.CreateModel.HeaderText = "tripathi";
            viewModel = m;
            var s = new GenericControl {
                
                DataContext = viewModel
            };
        }

    }

and below is the user control what I am creating from code behind:

public GenericControl()
        {
            this.InitializeComponent();
            //this.DataContext = this;
            DataContextChanged += (s, e) =>
            {
                var createModel = this.DataContext as TestViewModel;
                CreateControl(createModel.CreateModel);
            };
        }

        private void CreateControl(Test t) {
            var grid = new Grid();
            grid.SetBinding(Grid.DataContextProperty, new Binding { Source = DataContext, Mode = BindingMode.TwoWay });
            //grid.DataContext = this.DataContext;
            var tb = new TextBlock();
            tb.SetBinding(TextBlock.TextProperty, new Binding { Source = t.HeaderText, Mode = BindingMode.TwoWay
                 , UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged});

            grid.Children.Add(tb);
            gridlayout.Children.Add(grid);
        }

GenericControl.xaml:

<Grid Name="gridlayout">

    </Grid>

TestViewModel:

private Test _createModel = new Test();

        private string _textHeader;

        public Test CreateModel
        {
            get { return _createModel; }
            set { _createModel = value;
                Task.Factory.StartNew(() =>
                {
                    Thread.Sleep(5000);
                RaisePropertyChanged("CreateModel");

                });
            }
        }

        public TestViewModel() {
            CreateModel.HeaderText = "Ankit";
            CreateModel = CreateModel;
        }

        public event PropertyChangedEventHandler PropertyChanged;
        public void RaisePropertyChanged([CallerMemberName]string prop = "")
        {
            if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(prop));
        }

Model:

private string _headerText;

        public string HeaderText
        {
            get { return _headerText; }
            set { _headerText = value;
                Task.Factory.StartNew(() =>
                {
                    Thread.Sleep(10000);
                    RaisePropertyChanged("HeaderText");

                });

            }
        }



        public event PropertyChangedEventHandler PropertyChanged;
        public void RaisePropertyChanged([CallerMemberName]string prop = "") {
            if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(prop));
        }

I am getting correct values on debugging, but it is not showing in UI.


Solution

  • The main issue here for the binding not working is that you should not set the Source property of the binding here:

    tb.SetBinding(
      TextBlock.TextProperty, 
      new Binding { 
        Source = t.HeaderText, 
        Mode = BindingMode.TwoWay, 
        UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged
      });
    

    As doing so is basically saying to the Binding engine to observe the string instance that is in t.HeaderText at the creation of the binding, which is most likely null, and cannot change as it string is not observable.

    What you'll want is to do the following instead:

    tb.SetBinding(
      TextBlock.TextProperty,
      new Binding { 
        Path = new PropertyPath("CreateModel.HeaderText"), 
        Mode = BindingMode.TwoWay, 
        UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged
      });
    

    This will instruct the binding engine to se the current DataContext when the TextBlock is added into gridLayout, which is of type TestViewModel.

    You also do not need this line:

    grid.SetBinding(
      Grid.DataContextProperty, 
      new Binding { 
        Source = DataContext, 
        Mode = BindingMode.TwoWay 
      });
    

    As this is done automatically by the framework.

    You should also be careful about creating UI on the DataContext changed and not clearing it, where:

    gridlayout.Children.Add(grid);
    

    will continuously add TextBlock instances to the gridLayout, without removing previous ones.

    And finally, you should not raise event changes this way:

    set { 
        _createModel = value;
        Task.Factory.StartNew(() =>
        {
            Thread.Sleep(5000);
            RaisePropertyChanged("CreateModel");
    
        });
    }
    

    as you'll raise from a non-UI thread, and will cause an exception. Make sure to use Dispatcher.RunAsync instead.