1

Going through:

WPF binding not updating the view https://learn.microsoft.com/en-us/dotnet/api/system.componentmodel.inotifypropertychanged?redirectedfrom=MSDN&view=netcore-3.1

and

WPF DataContext updated but UI not updated

I still can't see why the UI is not updating in the following case (my best guess is that the DataContext of the Grid to be updated is not updated) and am loosing my mind:

AppliedJobsModel.cs (has IPropertyChange implemented as some of the answers suggest):

public class AppliedJobsModel { }
public class AppliedJob : INotifyPropertyChanged
{
    private string appliedDate;
    private string url;
    private string company;
    private string description;
    private string contact;
    private string stack;
    private string response;
    private string interviewDate;

    public AppliedJob(string[] entries)
    {
        appliedDate = entries[Consts.APPLIED_DATE_INDEX];
        url = entries[Consts.URL_INDEX];
        company = entries[Consts.COMPANY_INDEX];
        description = entries[Consts.DESCRIPTION_INDEX];
        contact = entries[Consts.CONTACT_INDEX];
        stack = entries[Consts.STACK_INDEX];
        response = entries[Consts.RESPONSE_INDEX];
        interviewDate = entries[Consts.INTERVIEWDATE_INDEX];
    }

    public string AppliedDate
    {
        get {
            return appliedDate;
        }

        set {
            if (appliedDate != value)
            {
                appliedDate = value;
                RaisePropertyChanged("AppliedDate");
            }
        }
    }

    public string Url
    {
        get
        {
            return url;
        }

        set
        {
            if (url != value)
            {
                url = value;
                RaisePropertyChanged("Url");
            }
        }
    }

    public string Company
    {
        get
        {
            return company;
        }

        set
        {
            if (company != value)
            {
                company = value;
                RaisePropertyChanged("Company");
            }
        }
    }

    public string Description
    {
        get
        {
            return description;
        }

        set
        {
            if (description != value)
            {
                description = value;
                RaisePropertyChanged("Description");
            }
        }
    }

    public string Contact
    {
        get
        {
            return contact;
        }

        set
        {
            if (contact != value)
            {
                contact = value;
                RaisePropertyChanged("Contact");
            }
        }
    }

    public string Stack
    {
        get
        {
            return stack;
        }

        set
        {
            if (stack != value)
            {
                stack = value;
                RaisePropertyChanged("Stack");
            }
        }
    }

    public string Response
    {
        get
        {
            return response;
        }

        set
        {
            if (response != value)
            {
                response = value;
                RaisePropertyChanged("Response");
            }
        }
    }

    public string InterviewDate
    {
        get
        {
            return interviewDate;
        }

        set
        {
            if (interviewDate != value)
            {
                interviewDate = value;
                RaisePropertyChanged("InterviewDate");
            }
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;

    private void RaisePropertyChanged(string property)
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(property));
        }
    }
}

AppliedJobsViewModel.cs (has an observable collection that gets correctly updated when a button is clicked (in dbg)):

class AppliedJobsViewModel
{
    private TexParser texParser;
   

    public AppliedJobsViewModel() {
        // TODO:
        // -- do nothing here 
    }

    public ObservableCollection<AppliedJob> AppliedJobsCollection
    {
        get;
        set;
    }

    private ICommand _openTexClick;
    public ICommand OpenTexClick
    {
        get
        {
            return _openTexClick ?? (_openTexClick = new CommandHandler(() => ReadAndParseTexFile(), () => CanExecute));
        }
    }
    public bool CanExecute
    {
        get
        {
            // check if executing is allowed, i.e., validate, check if a process is running, etc. 
            return true;
        }
    }

    public async Task ReadAndParseTexFile()
    {
        if (texParser == null)
        {
            texParser = new TexParser();
        }
        // Read file asynchronously here
        await Task.Run(() => ReadFileAndUpdateUI());            
    }

    private void ReadFileAndUpdateUI()
    {
        texParser.ReadTexFile();
        string[][] appliedJobsArray = texParser.getCleanTable();
        // Use this: 
        // https://rachel53461.wordpress.com/2011/09/17/wpf-grids-rowcolumn-count-properties/
        // Update collection here
        List<AppliedJob> appliedJobsList = createAppliedJobsListFromTable(appliedJobsArray);
        AppliedJobsCollection = new ObservableCollection<AppliedJob>(appliedJobsList);
    }

    private List<AppliedJob> createAppliedJobsListFromTable(string[][] table)
    {
        List<AppliedJob> jobsList = new List<AppliedJob>();

        for (int i = 0; i < table.Length; i++)
        {
            jobsList.Add(new AppliedJob(table[i]));
        }

        return jobsList;
    }
}

AppliedJobsView.xaml:

<UserControl x:Class="JobTracker.Views.AppliedJobsView"
         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:JobTracker.Views"
         mc:Ignorable="d" 
         d:DesignHeight="450" d:DesignWidth="800">
<Grid Name="appliedJobsGrid" Grid.Row="1" Grid.Column="1" Background="#50000000" Margin="10,10,10,10">
    <ItemsControl ItemsSource = "{Binding Path = AppliedJobsCollection}">
        <ItemsControl.ItemTemplate>
            <DataTemplate>
                <StackPanel Orientation = "Horizontal">
                    <TextBox Text = "{Binding Path = AppliedDate, Mode = TwoWay}" Width = "100" />

                    <TextBox Text = "{Binding Path = Url, Mode = TwoWay}"  Width = "100" />

                    <TextBox Text = "{Binding Path = Company, Mode = TwoWay}"  Width = "100" />

                    <TextBox Text = "{Binding Path = Description, Mode = TwoWay}"  Width = "100" />

                    <TextBox Text = "{Binding Path = Contact, Mode = TwoWay}"  Width = "100" />

                    <TextBox Text = "{Binding Path = Stack, Mode = TwoWay}"  Width = "100" />

                    <TextBox Text = "{Binding Path = Response, Mode = TwoWay}"  Width = "100" />

                    <TextBox Text = "{Binding Path = InterviewDate, Mode = TwoWay}"  Width = "100" />
                </StackPanel>
            </DataTemplate>
        </ItemsControl.ItemTemplate>
    </ItemsControl>
</Grid>

TrackerHome.xaml (main page/uses the user control):

<Grid Grid.Row="1" Grid.Column="1">
    <views:AppliedJobsView x:Name = "AppliedJobsControl" Loaded = "AppliedJobsViewControl_Loaded" />
</Grid>

TrackerHome.cs:

    public TrackerHome()
    {
        InitializeComponent();
        // Set data context here (https://stackoverflow.com/questions/12422945/how-to-bind-wpf-button-to-a-command-in-viewmodelbase)
        // https://stackoverflow.com/questions/33929513/populate-a-datagrid-using-viewmodel-via-a-database
        if (appliedJobsViewModel == null)
        {
            appliedJobsViewModel = new AppliedJobsViewModel();                
        }
        this.DataContext = appliedJobsViewModel;
        //AppliedJobControl.DataContext = appliedJobsViewModel;
    }

    private void AppliedJobsViewControl_Loaded(object sender, RoutedEventArgs e)
    {
        if (appliedJobsViewModel == null)
        {
            appliedJobsViewModel = new AppliedJobsViewModel();                
        }
        AppliedJobsControl.DataContext = appliedJobsViewModel;
    }
Sebi
  • 4,262
  • 13
  • 60
  • 116
  • 1
    What is not updating and how do you see it's not updating (how to reproduce the issue)? – Sinatr Oct 06 '20 at 14:01
  • After click read tex file, an observable collection should be updated with a number of AppliedJob objects. These objects should be rendered into a grid of text boxes onto the UI. I'm suspecting a datacontext related issue (viewmodel not instantiated at the right time) similar to https://stackoverflow.com/questions/24847062/how-can-i-access-my-viewmodel-from-code-behind – Sebi Oct 06 '20 at 14:03
  • 1
    `AppliedJobsCollection` doesn't seem to raise the `PropertyChanged` event? – mm8 Oct 06 '20 at 14:06
  • Yes, the collection doesn't get "noticed" at all. – Sebi Oct 06 '20 at 14:44

1 Answers1

1

You are setting a new value of property here:

AppliedJobsCollection = new ObservableCollection<AppliedJob>(appliedJobsList);

but it's a simple auto-property without notification.

Make it full property (view model needs to implement INotifyPropertyChange):

ObservableCollection<AppliedJob> _appliedJobsCollection =
    new ObservableCollection<AppliedJob>(); // empty initially
public ObservableCollection<AppliedJob> AppliedJobsCollection
{
    get => _appliedJobsCollection;
    set
    {
        _appliedJobsCollection = value;
        RaisePropertyChanged(nameof(AppliedJobsCollection));
    }
}

How does the full property behave? Is it as if all entries in each item in the collection have been changed (and thus have their properties changed)?

See this pseudo-code.

// given that AppliedJobsCollection is already initialized

// modify existing collection -> works
// bindings was subscribed to CollectionChanged event and will update
AppliedJobsCollection.Add(new AppliedJob(...));

// change item property -> works
// you implement INotifyPropertyChanged for items
// bindings was subscribed to that and will update
AppliedJobsCollection[0].Company = "bla";

// new instance of collection -> ... doesn't works
// how bindings can update?
AppliedJobsCollection = new ObservableCollection<AppliedJob>(...);

For last scenario to work you need to implement INotifyPropertyChanged for a class containing AppliedJobsCollection property and rise notification.

Sinatr
  • 20,892
  • 15
  • 90
  • 319
  • 2
    And give it a collection initially rather than just null. – Andy Oct 06 '20 at 14:21
  • But what if the collection doesn't exist yet? The entries are parsed from a file. I'll leave it as empty instead. – Sebi Oct 06 '20 at 14:44
  • @Sinatr Yes, the collection is created upon parsing the file. How does the full property behave? Is it as if all entries in each item in the collection have been changed (and thus have their properties changed)? – Sebi Oct 06 '20 at 14:46
  • 1
    You mean the data doesn't exist. A collection can be empty as in backer variable = new ObservableCollection() That's a collection. With no data in it. Or you can ignore my advice. But don't be too surprised when nothing shows up in the itemscontrol. – Andy Oct 06 '20 at 14:54
  • @Andy and Sinatr Thanks for helping me navigate through this nightmare :-) – Sebi Oct 07 '20 at 12:47