In this, the final part of the series, we'll implement data persistence, as well as some features which will allow users to edit existing entries.
We'll begin by adding a storage folder to our project. While you could place the storage folder wherever you would like, I recommend placing it within the 'ViewModels' folder to follow along. The file structure of your solution should look as follows.
Solution 'MVVMIntro' (1 of 1 project)
MVVMIntroUI
Dependencies
Models
ViewModels
Storage
Views
App.xaml
AssemblyInfo.cs
MainWindow.xaml
Next, we'll override the 'ToString' method of our person model, which will allow us to easily write our data to the file in a format that will be easy to deserialize.
Open 'Person.cs', and add the following override to the bottom of the class.
namespace MVVMIntroUI.Models
{
class Person
{
...
public override string ToString()
{
return $"{this.FirstName},{this.LastName},{this.Email}";
}
}
}
We'll then focus on adding the logic for both reading and writing our data, as well as editing existing entries to our people view-model. Open 'PeopleViewModel.cs' and add make the necessary changes. I'll leave out the property definitions for the first name, last name, and email text boxes, as they are unchanged.
namespace MVVMIntroUI.ViewModels
{
class PeopleViewModel : NotificationBase
{
private const string FILE_PATH = "../../../ViewModels/Storage/data.txt";
public ObservableCollection<Person> People { get; set; }
private Person _selectedPerson;
public Person SelectedPerson
{
get { return this._selectedPerson; }
set
{
this._selectedPerson = value;
this.OnPropertyChanged(nameof(this.SelectedPerson));
}
}
...
public Command OnSubmitCommand { get; set; }
public Command OnUpdateCommand { get; set; }
public Command OnRemoveCommand { get; set; }
public PeopleViewModel()
{
this.People = new ObservableCollection<Person>();
if (!File.Exists(FILE_PATH))
File.Create(FILE_PATH);
using (StreamReader sr = new StreamReader(FILE_PATH))
{
string line;
while ((line = sr.ReadLine()) != null)
{
string[] personData = line.Split(',');
this.People.Add(new Person(personData[0], personData[1], personData[2]));
}
}
this.OnSubmitCommand = new Command(this.OnSubmit);
this.OnUpdateCommand = new Command(this.OnUpdate);
this.OnRemoveCommand = new Command(this.OnRemove);
}
public void OnSubmit()
{
this.People.Add(
new Person
(
this.FirstNameTextBox,
this.LastNameTextBox,
this.EmailTextBox
)
);
this.EmptyTextBoxes();
this.Save();
}
public void OnUpdate()
{
if (this.SelectedPerson != null)
{
Person person = new Person(this.FirstNameTextBox, this.LastNameTextBox, this.EmailTextBox);
int index = this.People.IndexOf(this.SelectedPerson);
this.People.Remove(this.SelectedPerson);
this.People.Insert(index, person);
this.EmptyTextBoxes();
this.Save();
}
}
public void OnRemove()
{
if (this.SelectedPerson != null)
{
this.People.Remove(this.SelectedPerson);
this.Save();
}
}
private void EmptyTextBoxes()
{
this.FirstNameTextBox = string.Empty;
this.LastNameTextBox = string.Empty;
this.EmailTextBox = string.Empty;
}
private void Save()
{
using (StreamWriter sw = new StreamWriter(FILE_PATH))
{
foreach (Person person in this.People)
{
sw.WriteLine(person);
}
}
}
}
}
We've added a constant path to a 'data.txt' file. You'll notice that upon instantiation of the class, we're checking to see if this file exists, and creating it in the event that it does not. We then capture each line of the file, splitting the line at each comma before storing it in an array. Finally, we add a new instance of the person class to our people collection, initializing it with the data stored in the array.
We also have a new person property named 'SelectedPerson'. This will allow us to track which person is currently selected from the data grid of our people view.
There are two new commands, which will allow us to call the 'OnUpdate' and 'OnRemove' methods from the people view, as we did with the 'OnSubmit' method.
Aside from the two aforementioned methods, we have two new private methods as well. The 'EmptyTextBoxes' method is self-explanatory, so we'll focus on the 'Save' method. The 'Save' method simply iterates through the people collection and writes each member to a line of the file. Since we overrode the 'ToString' method of the person class, we just need to pass the member to the 'WriteLine' method of the stream writer, and it will write the member to the file in the appropriate format.
The 'OnUpdate' method first checks that the selection exists. If the selection exists, we create a new person instance with our text box values, and replace our selected person with the new instance before removing the selected person, and calling the 'EmptyTextBoxes' and 'Save' methods. If our person class inherited from our notification base class, we could track changes to the properties of individual members in the people collection and change them, rather than insert a new instance, however, this slightly hacky workaround allows us to keep our program structure the way it is.
The 'OnRemove' method is quite simple. We merely check that the selection exists; if it does, we remove it from the collection and call the 'Save' method.
Now that our people view-model is sorted, we just need to modify our people view. As usual, don't worry too much about the layout, and feel free to arrange things however you would like. Be sure to add the two new buttons and make some necessary changes to the data grid.
<UserControl x:Class="MVVMIntroUI.Views.PeopleView"
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:vm="clr-namespace:MVVMIntroUI.ViewModels"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<UserControl.Resources>
<vm:PeopleViewModel x:Key="PeopleViewModel" />
</UserControl.Resources>
<Grid DataContext="{StaticResource PeopleViewModel}"
Margin="5">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="auto" />
</Grid.RowDefinitions>
<DataGrid Grid.Row="0"
ItemsSource="{Binding People}"
AutoGenerateColumns="False"
CanUserAddRows="False"
CanUserDeleteRows="False"
IsReadOnly="True"
SelectionMode="Single"
RowHeaderWidth="0"
SelectedItem="{Binding SelectedPerson, Mode=OneWayToSource}">
<DataGrid.Columns>
<DataGridTextColumn Width="*" Header="First Name" Binding="{Binding FirstName}" />
<DataGridTextColumn Width="*" Header="Last Name" Binding="{Binding LastName}" />
<DataGridTextColumn Width="*" Header="Email" Binding="{Binding Email}" />
</DataGrid.Columns>
</DataGrid>
<Grid Grid.Row="1">
<Grid.RowDefinitions>
<RowDefinition Height="auto" />
<RowDefinition Height="auto" />
<RowDefinition Height="auto" />
<RowDefinition Height="auto" />
<RowDefinition Height="auto" />
</Grid.RowDefinitions>
<TextBox Grid.Row="0"
Margin="0 5 0 0"
Text="{Binding FirstNameTextBox, Mode=TwoWay}" />
<TextBox Grid.Row="1"
Margin="0 5 0 0"
Text="{Binding LastNameTextBox, Mode=TwoWay}" />
<TextBox Grid.Row="2"
Margin="0 5 0 0"
Text="{Binding EmailTextBox, Mode=TwoWay}" />
<Button Grid.Row="3"
Margin=" 0 5 0 0"
Content="Submit"
Command="{Binding OnSubmitCommand}" />
<Grid Grid.Row="4"
Margin="0 5 0 0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Button Grid.Column="0"
Margin="0 0 2.5 0"
Content="Update"
Command="{Binding OnUpdateCommand}" />
<Button Grid.Column="1"
Margin="2.5 0 0 0"
Content="Remove"
Command="{Binding OnRemoveCommand}" />
</Grid>
</Grid>
</Grid>
</UserControl>
My view has a bunch of unnecessary additions which helps to make the UI look a little cleaner. Your data grid could be as simple as follows.
<DataGrid Grid.Row="0"
ItemsSource="{Binding People}"
CanUserAddRows="False"
CanUserDeleteRows="False"
IsReadOnly="True"
SelectionMode="Single"
SelectedItem="{Binding SelectedPerson, Mode=OneWayToSource}" />
There's always more to learn, but, at this point, you should be comfortable with the most common aspects of utilizing the MVVM pattern in a WPF application.
As a bonus, let's add dynamic behavior to our new buttons. Open 'PersonViewModel.cs', and Add the following property.
private bool _areManipulatorButtonsAvailable;
public bool AreManipulatorButtonsAvailable
{
get { return this._areManipulatorButtonsAvailable; }
set
{
this._areManipulatorButtonsAvailable = value;
this.OnPropertyChanged(nameof(this.AreManipulatorButtonsAvailable));
}
}
Now, we'll set the value of our new property within the setter method of the 'SelectedPerson' property.
public Person SelectedPerson
{
get { return this._selectedPerson; }
set
{
this._selectedPerson = value;
this.AreManipulatorButtonsAvailable = value != null;
this.OnPropertyChanged(nameof(this.SelectedPerson));
}
}
Finally, we merely bind the 'IsEnabled' attribute of our new buttons to the new property.
<Button Grid.Column="0"
Content="Update"
IsEnabled="{Binding AreManipulatorButtonsAvailable}"
Command="{Binding OnUpdateCommand}" />
<Button Grid.Column="1"
Content="Remove"
IsEnabled="{Binding AreManipulatorButtonsAvailable}"
Command="{Binding OnRemoveCommand}" />
And, as simple as that, our new buttons will only be enabled when a member of the collection is selected.
I hope some of you were able to find this series helpful, and you should now have the knowledge you need to begin building a WPF application of your own using the MVVM pattern.
Top comments (0)