Question Logic in class breaking binding

ConsKa

Well-known member
Joined
Dec 11, 2020
Messages
140
Programming Experience
Beginner
So after a bit of a wrangle I managed to write some code that would allow me to read a text file, split it and place it into an array and then bind that array to text boxes - it isn't great, as there are 60 entries in the Array, and splitting them into the textboxes was a pain - but it works. I am able to populate all text boxes with the correct data from a text file and the numbers in the text file do not change - it is always 60.

C#:
List<string> Entries = new List<string>();

string DS1 = fn.CombinedName("DataSet1.txt");

            if (File.Exists(DS1))
            {
                var lines = File.ReadAllLines(DS1);
                
                Entries= lines.SelectMany(x => x.Split(new[]
                {
                    ','
                }, StringSplitOptions.None)).ToList();
            
            }            
TimeKeeper.DataContext = this;

I was then able to bind a text box to the output in xaml like so:

C#:
Text="{Binding Entries [0]}"
Text="{Binding Entries [5]}"

This is all in a grid called TimeKeeper.

There are 5 entries per line, so 0 & 5 is the first of x and then the second of x.

So trying to follow the proper course, rather than have that in my Main Window code, I created a class, and put the logic in the class.

C#:
public class DataCollection
    {
        public List<string> Populate { get; set; }
        FileName fn = new FileName();

        public List<string> Entries()
        {
            string DS1 = fn.CombinedName("DataSet1.txt");

            if (File.Exists(DS1))
            {
                var lines = File.ReadAllLines(DS1);
                
                Populate = lines.SelectMany(x => x.Split(new[]
                {
                    ','
                }, StringSplitOptions.None)).ToList();
            }
            return Populate;
        }
    }

Then in the main window

C#:
List<string> Entries = new List<string>();

private void Reload()
        {
            DataCollection dc = new DataCollection();
            dc.Entries();

            Entries = dc.Populate;

            TimeKeeper.DataContext = Entries;
        }

Using breakpoints, I can see that Entries has 60 entries, as it is supposed to have, and the binding is the same - I just don't know why it isn't working. I tried

C#:
TimerKeeper.DataContext = this;

as it was previously, but that didn't work either.

Am I missing something that I now need to bind this after moving the logic to a class?
 
I could be confused and mistaken right now. I'm running a low grade fever. There may have been somebody else who was on the same journey you are where they were trying read in a text file and the get the data assigned to particular controls on their WinForms controls.

Kudos to you though for giving WPF another try! The initial learning curve is steeper than the WinForms learning curve, but the techniques and mindset that you pick up as a result of that learning leads to cleaner and more modular architectures.
 
As I mentioned in the other thread, some sample code for loading using data binding without refering to any controls. Below, I'm binding data into a DataGrid:
MainWindow.xaml:
<Window x:Class="WpfDataGrid.MainWindow"
        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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfDataGrid"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>

        <DataGrid Grid.Row="0" ItemsSource="{Binding Jobs}" />

        <StackPanel Grid.Row="1" Orientation="Horizontal" Margin="5">
            <Button Command="{Binding LoadCommand}" Margin="5">Load</Button>
            <Button Command="{Binding SaveCommand}" Margin="5">Save</Button>
        </StackPanel>
    </Grid>
</Window>

MainWindow.xaml.cs:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace WpfDataGrid
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            DataContext = new MainViewModel(new JobsRepository());
        }
    }
}

Job.cs:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace WpfDataGrid
{
    class Job
    {
        public string Id { get; set; }
        public string Code { get; set; }
        public string Name { get; set; }
        public string Description { get; set; }
        public string Time { get; set; }
    }
}

JobsRepository.cs:
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace WpfDataGrid
{
    class JobsRepository
    {
        const string JobsFile = "DataSet1.json";

        public bool DoesJobsFileExists()
            => File.Exists(JobsFile);

        public IEnumerable<Job> LoadJobs()
        {
            if (!DoesJobsFileExists())
                return new List<Job>();

            var json = File.ReadAllText(JobsFile);
            var jobs = JsonConvert.DeserializeObject<List<Job>>(json);
            return jobs;
        }

        public void SaveJobs(IEnumerable<Job> jobs)
        {
            var json = JsonConvert.SerializeObject(jobs, Formatting.Indented);
            File.WriteAllText(JobsFile, json);
        }
    }
}

MainWindowViewModel.cs:
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;

namespace WpfDataGrid
{
    class MainViewModel : INotifyPropertyChanged
    {
        JobsRepository _jobsRepository;
        ObservableCollection<Job> _jobs;

        public ObservableCollection<Job> Jobs
        {
            get { return _jobs; }
            set { _jobs = value; NotifyPropertyChanged(); }
        }

        public ICommand LoadCommand { get; }
        public ICommand SaveCommand { get; }

        public event PropertyChangedEventHandler PropertyChanged;

        public MainViewModel(JobsRepository jobsRepository)
        {
            _jobsRepository = jobsRepository;

            LoadCommand = new RelayCommand(
                    p => Jobs = new ObservableCollection<Job>(_jobsRepository.LoadJobs()),
                    p => _jobsRepository.DoesJobsFileExists()
                );
            SaveCommand = new RelayCommand(p => jobsRepository.SaveJobs(Jobs));

            Jobs = new ObservableCollection<Job>(_jobsRepository.LoadJobs().ToList());

        }

        void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
            => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

RelayCommand.cs:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;

namespace WpfDataGrid
{
    public class RelayCommand : ICommand
    {
        private Action<object> _execute;
        private Func<object, bool> _canExecute;

        public event EventHandler CanExecuteChanged
        {
            add { CommandManager.RequerySuggested += value; }
            remove { CommandManager.RequerySuggested -= value; }
        }

        public RelayCommand(Action<object> execute, Func<object, bool> canExecute = null)
        {
            this._execute = execute;
            this._canExecute = canExecute;
        }

        public bool CanExecute(object parameter)
            => _canExecute == null || _canExecute(parameter);

        public void Execute(object parameter)
            => _execute(parameter);
    }
}

It's developer UI, so nothing to write home about since the main focus here is about how to do databinding, not WPF styling.

The key takeaway here is that each row on the DataGrid represents a Job object. The logic for loading and saving the list of Jobs falls on the JobsRepository class which just uses Newtonsoft's JSON.NET Nuget library. The MainViewModel takes care of coordinating loading stuff into the UI (hence the INotifyPropertyChanged implementation to notify the UI that the Jobs property might have changed), or routing commands to the repository for loading or saving data.

When you first run the program, it will start off with one empty row, but you can quickly just create 12 blank rows and then click the Save button. The next time your run the program, if it finds data file, it will load it up automatically.

Screenshot_1.png
 
Thanks for taking the time to write that.

I have been reading a fair amount and watched a couple of tutorials, and I am beginning to get a better grasp on things generally - which I think is a bit of a first step - the syntax isn't really the difficulty here as I see it (just memorising words and meanings after all and that just requires time) it is really getting a handle on the way it is supposed to work - so appreciate you writing that out so I can see it in the context of what I am doing.

I recognise certain parts you have drafted above - seen them in other scenarios - watched a rather useful tutorial recently, while it wasn't great as a standalone tutorial, what it did manage to do was bring together a couple of things I had been learning about which are replicated in your code above.

I did manage to break your code because I added public to the classes on the belief that you had put this together for me in a hurry and perhaps not added the access modifiers, only to find that no...they need to be private - I am not sure why that is, will have to investigate a little.

I have it up and running, off to work - yes on a Sunday , but will pour over this a little later to get a better understanding of it.

Thanks again.
 
In C#, if you do not specify a access modifier, then the member or type is considered private. As I recall, the only exception used to be interface members which are public by default. I think some of the newer C# 7 or 8 types might have somethings as public by default.
 
I knew that access modifiers defaulted to private, which is why I changed it to public, thinking you simply omitted it to get it done - I know you were not feeling well - but it breaks if you make them public, says that there is Inconsistent accessibility.

This was also due to all my learning around classes the first thing everyone does is make their class public....I am guessing for demonstration purposes...but for newbs it is easy to take the point that a class should be public.

I am guessing that if you needed it public, you could go through and change all of the accessibility on those parts - but works as you wrote it...
 
I only make things public when something else actually needs access to it. YAGNI principle applied to access modifiers.
 
Back
Top Bottom