Virtual Storage Spaces and extension methods
Let's start by discussing the logic behind a Virtual Storage Space. This is a single virtual representation of several physical spaces on your hard drive (or hard drives). A storage space will be seen as a single area where a specific group of eBooks are stored. I use the term stored loosely because the storage space doesn't exist. It represents more of a grouping than a physical space on the hard drive:
- To start creating Virtual Storage Spaces, add a new class called
StorageSpace
to the eBookManager.Engine
project. Open the StorageSpace.cs
file and add the following code to it:
using System;
using System.Collections.Generic;
namespace eBookManager.Engine
{
[Serializable]
public class StorageSpace
{
public int ID { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public List<Document> BookList { get; set; }
}
}
Note
Note that you need to include the System.Collections.Generic
namespace here, because the StorageSpace
class contains a property called BookList
of type List<Document>
that will contain all the books in that particular storage space.
Now we need to focus our attention on the ExtensionMethods
class in the eBookManager.Helper
project. This will be a static class, because extension methods need to be static in nature in order to act on the various objects defined by the extension methods.
Add a new class to the eBookManager.Helper
project and modify the ExtensionMethods
class as follows:
public static class ExtensionMethods
{
}
Let's add the first extension method to the class called ToInt()
. What this extension method does is take a string
value and try to parse it to an integer
value. I am too lazy to type Convert.ToInt32(stringVariable)
whenever I need to convert a string
to an integer
. It is for this reason that I use an extension method.
- Add the following static method to the
ExtensionMethods
class:
public static int ToInt(this string value, int defaultInteger = 0)
{
try
{
if (int.TryParse(value, out int validInteger))
// Out variables
return validInteger;
else
return defaultInteger;
}
catch
{
return defaultInteger;
}
}
The ToInt()
extension method acts only on string
. This is defined by the code this string value
in the method signature where value
is the variable name that will contain the string
you are trying to convert to an integer
. It also has a default parameter called defaultInteger
, which is set to 0.
Unless the developer calling the extension method wants to return a default integer value of 0
, they can pass a different integer to this extension method (-1
, for example).
It is also here that we find our first feature of C# 7. The improvement to out
variables. In previous iterations of C#, we had to do the following with out
variables:
int validInteger;
if (int.TryParse(value, out validInteger))
{
}
There was this predeclared integer variable hanging around and it gets its value if the string
value parsed to an integer
. C# 7 simplifies the code a lot more:
if (int.TryParse(value, out int validInteger))
C# 7 allows developers to declare an out
variable right there where it is passed as an out
argument. Moving along to the other methods of the ExtensionMethods
class, these methods are used to provide the following logic:
Read
and write
to the data source
Check whether a storage space exists
Convert bytes to megabytes
Convert a string
to an integer
(as discussed previously)
The ToMegabytes
method is quite easy. Not having to write this calculation all over the place, defining it inside an extension method makes sense:
public static double ToMegabytes(this long bytes)
{
return (bytes > 0) ? (bytes / 1024f) / 1024f : bytes;
}
We also need a way to check if a particular storage space already exists.
Note
Be sure to add a project reference to eBookManager.Engine
from the eBookManager.Helper
project.
What this extension method also does is to return the next storage space ID to the calling code. If the storage space does not exist, the returned ID will be the next ID that can be used when creating a new storage space:
public static bool StorageSpaceExists(this List<StorageSpace> space, string nameValueToCheck, out int storageSpaceId)
{
bool exists = false;
storageSpaceId = 0;
if (space.Count() != 0)
{
int count = (from r in space
where r.Name.Equals(nameValueToCheck)
select r).Count();
if (count > 0)
exists = true;
storageSpaceId = (from r in space
select r.ID).Max() + 1;
}
return exists;
}
We also need to create a method that will write
the data we have to a file after converting it to JSON:
public static void WriteToDataStore(this List<StorageSpace> value, string storagePath, bool appendToExistingFile = false)
{
JsonSerializer json = new JsonSerializer();
json.Formatting = Formatting.Indented;
using (StreamWriter sw = new StreamWriter(storagePath,
appendToExistingFile))
{
using (JsonWriter writer = new JsonTextWriter(sw))
{
json.Serialize(writer, value);
}
}
}
This method is rather self-explanatory. It acts on a List<StorageSpace>
object and will create the JSON data, overwriting a file defined in the storagePath
variable.
Lastly, we need to be able to read
the data back again into a List<StorageSpace>
object and return that to the calling code:
public static List<StorageSpace> ReadFromDataStore(this List<StorageSpace> value, string storagePath)
{
JsonSerializer json = new JsonSerializer();
if (!File.Exists(storagePath))
{
var newFile = File.Create(storagePath);
newFile.Close();
}
using (StreamReader sr = new StreamReader(storagePath))
{
using (JsonReader reader = new JsonTextReader(sr))
{
var retVal =
json.Deserialize<List<StorageSpace>>(reader);
if (retVal is null)
retVal = new List<StorageSpace>();
return retVal;
}
}
}
The method will return an emptyList<StorageSpace>
object and nothing is contained in the file. TheExtensionMethods
class can contain many more extension methods that you might use often. It is a great way to separate often-used code.
The purpose of this class is merely to provide supporting code to a document. In the eBookManager
application, I am going to use a single method called GetFileProperties()
that will (you guessed it) return the properties of a selected file. This class also only contains this single method. As the application is modified for your specific purposes, you can modify this class and add additional methods specific to documents.
The DocumentEngine
class introduces us to the next feature of C# 7 called tuples. What do tuples do exactly? It is often a requirement for a developer to return more than a single value from a method. Among other Solutions, you can of course use out
parameters, but these do not work in async
methods. Tuples provide a better way to do this.
Inside the DocumentEngine
class, add the following code:
public (DateTime dateCreated, DateTime dateLastAccessed, string fileName, string fileExtension, long fileLength, bool error) GetFileProperties(string filePath)
{
var returnTuple = (created: DateTime.MinValue,
lastDateAccessed: DateTime.MinValue, name: "", ext: "",
fileSize: 0L, error: false);
try
{
FileInfo fi = new FileInfo(filePath);
fi.Refresh();
returnTuple = (fi.CreationTime, fi.LastAccessTime, fi.Name,
fi.Extension, fi.Length, false);
}
catch
{
returnTuple.error = true;
}
return returnTuple;
}
The GetFileProperties()
method returns a tuple as (DateTime dateCreated, DateTime dateLastAccessed, string fileName, string fileExtension, long fileLength, bool error)
and allows us to inspect the values returned from the calling code easily.
Before I try to get the properties of the specific file, I initialize the tuple
by doing the following:
var returnTuple = (created: DateTime.MinValue, lastDateAccessed: DateTime.MinValue, name: "", ext: "", fileSize: 0L, error: false);
If there is an exception, I can return default values. Reading the file properties is simple enough using the FileInfo
class. I can then assign the file properties to the tuple
by doing this:
returnTuple = (fi.CreationTime, fi.LastAccessTime, fi.Name, fi.Extension, fi.Length, false);
The tuple
is then returned to the calling code where it will be used as required. We will have a look at the calling code next.
The ImportBooks
form does exactly what the name suggests. It allows us to create Virtual Storage Spaces and to import books into those spaces. The form design is as follows:
The TreeView
controls are prefixed with tv
, buttons with btn
, combo boxes with dl
, textboxes with txt
, and date time pickers with dt
. When this form loads, if there are any storage spaces defined then these will be listed in the dlVirtualStorageSpaces
combo box. Clicking on the Select source folder
button will allow us to select a source folder to look for eBooks.
If a storage space does not exist, we can add a new Virtual Storage Space by clicking the btnAddNewStorageSpace
button. This will allow us to add a name and description for the new storage space and click on the btnSaveNewStorageSpace
button.
Selecting an eBook from the tvFoundBooks
TreeView will populate the File details
group of controls to the right of the form. You can then add additional Book details
and click on the btnAddeBookToStorageSpace
button to add the book to our space:
- You need to ensure that the following namespaces are added to your
ImportBooks
class:
using eBookManager.Engine;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Windows.Forms;
using static eBookManager.Helper.ExtensionMethods;
using static System.Math;
- Next, let's start at the most logical place to begin with, which is the constructor
ImportBooks()
and the form variables. Add the following code above the constructor:
private string _jsonPath;
private List<StorageSpace> spaces;
private enum StorageSpaceSelection { New = -9999, NoSelection = -1 }
The usefulness of the enumerator will become evident later on in code. The _jsonPath
variable will contain the path to the file used to store our eBook information.
- Modify the constructor as follows:
public ImportBooks()
{
InitializeComponent();
_jsonPath = Path.Combine(Application.StartupPath,
"bookData.txt");
spaces = spaces.ReadFromDataStore(_jsonPath);
}
The _jsonPath
is initialized to the executing folder for the application and the file hard coded to bookData.txt
. You could provide a settings screen if you wanted to configure these settings, but I just decided to make the application use a hard-coded setting.
- Next, we need to add another enumerator that defines the file extensions that we will be able to save in our application. It is here that we will see another feature of C# 7 called
expression-bodied
properties.
Expression-bodied accessors, constructors, and finalizers
If the following expression looks intimidating, it's because it is using a feature introduced in C# 6 and expanded in C# 7:
private HashSet<string> AllowedExtensions => new HashSet<string>(StringComparer.InvariantCultureIgnoreCase) { ".doc",".docx",".pdf", ".epub" };
private enum Extention { doc = 0, docx = 1, pdf = 2, epub = 3 }
The preceding example returns a HashSet
of allowed file extensions for our application. These have been around since C# 6, but have been extended in C# 7 to include accessors, constructors, and finalizers. Let's simplify the examples a bit.
Assume that we had to modify the Document
class to set a field for _defaultDate
inside the class; traditionally, we would need to do this:
private DateTime _defaultDate;
public Document()
{
_defaultDate = DateTime.Now;
}
In C# 7, we can greatly simplify this code by simply doing the following:
private DateTime _defaultDate;
public Document() => _defaultDate = DateTime.Now;
This is perfectly legal and compiles correctly. Similarly, the same can be done with finalizers (or deconstructors). Another nice implementation of this is expression-bodied
properties as seen with the AllowedExtensions
property. The expression-bodied
properties have actually been around since C# 6, but who's counting?
Suppose that we wanted to just return the string
value of the Extension
enumeration for PDFs, we could do something such as the following:
public string PDFExtension
{
get
{
return nameof(Extention.pdf);
}
}
That property only has a get accessor and will never return anything other than the string
value of Extension.pdf
. Simplify that by changing the code to:
public string PDFExtension => nameof(Extention.pdf);
That's it. A single line of code does exactly the same thing as the previous seven lines of code. Falling into the same category, expression-bodied
property accessors are also simplified. Consider the following 11 lines of code:
public string DefaultSavePath
{
get
{
return _jsonPath;
}
set
{
_jsonPath = value;
}
}
With C# 7, we can simplify this to the following:
public string DefaultSavePath
{
get => _jsonPath;
set => _jsonPath = value;
}
This makes our code a lot more readable and quicker to write. Swing back to our AllowedExtensions
property; traditionally, it would be written as follows:
private HashSet<string> AllowedExtensions
{
get
{
return new HashSet<string>
(StringComparer.InvariantCultureIgnoreCase) { ".doc",
".docx", ".pdf", ".epub" };
}
}
Since C# 6, we have been able to simplify this, as we saw previously. This gives developers a great way to reduce unnecessary code.
Populating the TreeView control
We can see the implementation of the AllowedExtensions
property when we look at the PopulateBookList()
method. All that this method does is populate the TreeView
control with files and folders found at the selected source location. Consider the following code:
public void PopulateBookList(string paramDir, TreeNode paramNode)
{
DirectoryInfo dir = new DirectoryInfo(paramDir);
foreach (DirectoryInfo dirInfo in dir.GetDirectories())
{
TreeNode node = new TreeNode(dirInfo.Name);
node.ImageIndex = 4;
node.SelectedImageIndex = 5;
if (paramNode != null)
paramNode.Nodes.Add(node);
else
tvFoundBooks.Nodes.Add(node);
PopulateBookList(dirInfo.FullName, node);
}
foreach (FileInfo fleInfo in dir.GetFiles().Where
(x => AllowedExtensions.Contains(x.Extension)).ToList())
{
TreeNode node = new TreeNode(fleInfo.Name);
node.Tag = fleInfo.FullName;
int iconIndex = Enum.Parse(typeof(Extention),
fleInfo.Extension.TrimStart('.'), true).GetHashCode();
node.ImageIndex = iconIndex;
node.SelectedImageIndex = iconIndex;
if (paramNode != null)
paramNode.Nodes.Add(node);
else
tvFoundBooks.Nodes.Add(node);
}
}
The first place we need to call this method is obviously from within itself, as this is a recursive method. The second place we need to call it is from the btnSelectSourceFolder
button click event:
private void btnSelectSourceFolder_Click(object sender, EventArgs e)
{
try
{
FolderBrowserDialog fbd = new FolderBrowserDialog();
fbd.Description = "Select the location of your eBooks and
documents";
DialogResult dlgResult = fbd.ShowDialog();
if (dlgResult == DialogResult.OK)
{
tvFoundBooks.Nodes.Clear();
tvFoundBooks.ImageList = tvImages;
string path = fbd.SelectedPath;
DirectoryInfo di = new DirectoryInfo(path);
TreeNode root = new TreeNode(di.Name);
root.ImageIndex = 4;
root.SelectedImageIndex = 5;
tvFoundBooks.Nodes.Add(root);
PopulateBookList(di.FullName, root);
tvFoundBooks.Sort();
root.Expand();
}
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
}
This is all quite straightforward code. Select the folder to recurse and populate the TreeView
control with all the files found that match the file extension contained in our AllowedExtensions
property.
We also need to look at the code when someone selects a book in the tvFoundBooks
TreeView
control. When a book is selected, we need to read the properties of the selected file and return those properties to the file details section:
private void tvFoundBooks_AfterSelect(object sender, TreeViewEventArgs e)
{
DocumentEngine engine = new DocumentEngine();
string path = e.Node.Tag?.ToString() ?? "";
if (File.Exists(path))
{
var (dateCreated, dateLastAccessed, fileName,
fileExtention, fileLength, hasError) =
engine.GetFileProperties(e.Node.Tag.ToString());
if (!hasError)
{
txtFileName.Text = fileName;
txtExtension.Text = fileExtention;
dtCreated.Value = dateCreated;
dtLastAccessed.Value = dateLastAccessed;
txtFilePath.Text = e.Node.Tag.ToString();
txtFileSize.Text = $"{Round(fileLength.ToMegabytes(),
2).ToString()} MB";
}
}
}
You will notice that it is here that we are calling the method GetFileProperties()
on the DocumentEngine
class that returns the tuple.
This is one of those features in C# 7 that I truly wondered where I would ever find a use for. As it turns out, local functions are extremely useful indeed. Also called nested functions by some, these functions are nested within another parent function. It is obviously only within scope inside the parent function and adds a useful way to call code that otherwise wouldn't have any real purpose outside the parent function. Consider the PopulateStorageSpacesList()
method:
private void PopulateStorageSpacesList()
{
List<KeyValuePair<int, string>> lstSpaces =
new List<KeyValuePair<int, string>>();
BindStorageSpaceList((int)StorageSpaceSelection.NoSelection,
"Select Storage Space");
void BindStorageSpaceList(int key, string value)
// Local function
{
lstSpaces.Add(new KeyValuePair<int, string>(key, value));
}
if (spaces is null || spaces.Count() == 0) // Pattern matching
{
BindStorageSpaceList((int)StorageSpaceSelection.New, "
<create new>");
}
else
{
foreach (var space in spaces)
{
BindStorageSpaceList(space.ID, space.Name);
}
}
dlVirtualStorageSpaces.DataSource = new
BindingSource(lstSpaces, null);
dlVirtualStorageSpaces.DisplayMember = "Value";
dlVirtualStorageSpaces.ValueMember = "Key";
}
To see how PopulateStorageSpacesList()
calls the local function BindStorageSpaceList()
, have a look at the following screenshot:
You will notice that the local function can be called from anywhere within the parent function. In this case, the BindStorageSpaceList()
local function does not return anything, but you can return whatever you like from a local function. You could just as well have done the following:
private void SomeMethod()
{
int currentYear = GetCurrentYear();
int GetCurrentYear(int iAddYears = 0)
{
return DateTime.Now.Year + iAddYears;
}
int nextYear = GetCurrentYear(1);
}
The local function is accessible from anywhere within the parent function.
Staying with the PopulateStorageSpacesList()
method, we can see the use of another C# 7 feature called pattern matching. The spaces is null
line of code is probably the simplest form of pattern matching. In reality, pattern matching supports several patterns.
Consider a switch
statement:
switch (objObject)
{
case null:
WriteLine("null"); // Constant pattern
break;
case Document doc when doc.Author.Equals("Stephen King"):
WriteLine("Stephen King is the author");
break;
case Document doc when doc.Author.StartsWith("Stephen"):
WriteLine("Stephen is the author");
break;
default:
break;
}
Pattern matching allows developers to use the is
expression to see whether something matches a specific pattern. Bear in mind that the pattern needs to check for the most specific to the most general pattern. If you simply started the case with case Document doc:
then all the objects passed to the switch
statement of type Document
would match. You would never find specific documents where the author is Stephen King
or starts with Stephen
.
For a construct inherited by C# from the C language, it hasn't changed much since the '70s. C# 7 changes all that with pattern matching.
Finishing up the ImportBooks code
Let's have a look at the rest of the code in the ImportBooks
form. The form load just populates the storage spaces list, if any existing storage spaces have been previously saved:
private void ImportBooks_Load(object sender, EventArgs e)
{
PopulateStorageSpacesList();
if (dlVirtualStorageSpaces.Items.Count == 0)
{
dlVirtualStorageSpaces.Items.Add("<create new storage
space>");
}
lblEbookCount.Text = "";
}
We now need to add the logic for changing the selected storage space. The SelectedIndexChanged()
event of the dlVirtualStorageSpaces
control is modified as follows:
private void dlVirtualStorageSpaces_SelectedIndexChanged(object sender, EventArgs e)
{
int selectedValue =
dlVirtualStorageSpaces.SelectedValue.ToString().ToInt();
if (selectedValue == (int)StorageSpaceSelection.New) // -9999
{
txtNewStorageSpaceName.Visible = true;
lblStorageSpaceDescription.Visible = true;
txtStorageSpaceDescription.ReadOnly = false;
btnSaveNewStorageSpace.Visible = true;
btnCancelNewStorageSpaceSave.Visible = true;
dlVirtualStorageSpaces.Enabled = false;
btnAddNewStorageSpace.Enabled = false;
lblEbookCount.Text = "";
}
else if (selectedValue !=
(int)StorageSpaceSelection.NoSelection)
{
// Find the contents of the selected storage space
int contentCount = (from c in spaces
where c.ID == selectedValue
select c).Count();
if (contentCount > 0)
{
StorageSpace selectedSpace = (from c in spaces
where c.ID ==
selectedValue
select c).First();
txtStorageSpaceDescription.Text =
selectedSpace.Description;
List<Document> eBooks = (selectedSpace.BookList ==
null)
? new List<Document> { } : selectedSpace.BookList;
lblEbookCount.Text = $"Storage Space contains
{eBooks.Count()} {(eBooks.Count() == 1 ? "eBook" :
"eBooks")}";
}
}
else
{
lblEbookCount.Text = "";
}
}
I will not go into any detailed explanation of the code here as it is relatively obvious what it is doing.
We also need to add the code to save a new storage space. Add the following code to the Click
event of the btnSaveNewStorageSpace
button:
private void btnSaveNewStorageSpace_Click(object sender,
EventArgs e)
{
try
{
if (txtNewStorageSpaceName.Text.Length != 0)
{
string newName = txtNewStorageSpaceName.Text;
// throw expressions: bool spaceExists =
(space exists = false) ? return false : throw exception
// Out variables
bool spaceExists = (!spaces.StorageSpaceExists
(newName, out int nextID)) ? false : throw new
Exception("The storage space you are
trying to add already exists.");
if (!spaceExists)
{
StorageSpace newSpace = new StorageSpace();
newSpace.Name = newName;
newSpace.ID = nextID;
newSpace.Description =
txtStorageSpaceDescription.Text;
spaces.Add(newSpace);
PopulateStorageSpacesList();
// Save new Storage Space Name
txtNewStorageSpaceName.Clear();
txtNewStorageSpaceName.Visible = false;
lblStorageSpaceDescription.Visible = false;
txtStorageSpaceDescription.ReadOnly = true;
txtStorageSpaceDescription.Clear();
btnSaveNewStorageSpace.Visible = false;
btnCancelNewStorageSpaceSave.Visible = false;
dlVirtualStorageSpaces.Enabled = true;
btnAddNewStorageSpace.Enabled = true;
}
}
}
catch (Exception ex)
{
txtNewStorageSpaceName.SelectAll();
MessageBox.Show(ex.Message);
}
}
Here, we can see another new feature in the C# 7 language called throw expressions. This gives developers the ability to throw exceptions from expressions. The code in question is this code:
bool spaceExists = (!spaces.StorageSpaceExists(newName, out int nextID)) ? false : throw new Exception("The storage space you are trying to add already exists.");
I always like to remember the structure of the code as follows:
The last few methods deal with saving eBooks in the selected Virtual Storage Space. Modify the Click
event of the btnAddBookToStorageSpace
button. This code also contains a throw expression. If you haven't selected a storage space from the combo box, a new exception is thrown:
private void btnAddeBookToStorageSpace_Click(object sender, EventArgs e)
{
try
{
int selectedStorageSpaceID =
dlVirtualStorageSpaces.SelectedValue.ToString().ToInt();
if ((selectedStorageSpaceID !=
(int)StorageSpaceSelection.NoSelection)
&& (selectedStorageSpaceID !=
(int)StorageSpaceSelection.New))
{
UpdateStorageSpaceBooks(selectedStorageSpaceID);
}
else throw new Exception("Please select a Storage
Space to add your eBook to"); // throw expressions
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
}
Developers can now immediately throw exceptions in code right there where they occur. This is rather nice and makes code cleaner and its intent clearer.
Saving a selected book to a storage space
The following code basically updates the book list in the selected storage space if it already contains the specific book (after confirming with the user). Otherwise, it will add the book to the book list as a new book:
private void UpdateStorageSpaceBooks(int storageSpaceId)
{
try
{
int iCount = (from s in spaces
where s.ID == storageSpaceId
select s).Count();
if (iCount > 0) // The space will always exist
{
// Update
StorageSpace existingSpace = (from s in spaces
where s.ID == storageSpaceId select s).First();
List<Document> ebooks = existingSpace.BookList;
int iBooksExist = (ebooks != null) ? (from b in ebooks
where $"{b.FileName}".Equals($"
{txtFileName.Text.Trim()}")
select b).Count() : 0;
if (iBooksExist > 0)
{
// Update existing book
DialogResult dlgResult = MessageBox.Show($"A book
with the same name has been found in Storage Space
{existingSpace.Name}.
Do you want to replace the existing book
entry with this one?",
"Duplicate Title", MessageBoxButtons.YesNo,
MessageBoxIcon.Warning,
MessageBoxDefaultButton.Button2);
if (dlgResult == DialogResult.Yes)
{
Document existingBook = (from b in ebooks
where $"
{b.FileName}".Equals($"
{txtFileName.Text.Trim()}")
select b).First();
existingBook.FileName = txtFileName.Text;
existingBook.Extension = txtExtension.Text;
existingBook.LastAccessed =
dtLastAccessed.Value;
existingBook.Created = dtCreated.Value;
existingBook.FilePath = txtFilePath.Text;
existingBook.FileSize = txtFileSize.Text;
existingBook.Title = txtTitle.Text;
existingBook.Author = txtAuthor.Text;
existingBook.Publisher = txtPublisher.Text;
existingBook.Price = txtPrice.Text;
existingBook.ISBN = txtISBN.Text;
existingBook.PublishDate =
dtDatePublished.Value;
existingBook.Category = txtCategory.Text;
}
}
else
{
// Insert new book
Document newBook = new Document();
newBook.FileName = txtFileName.Text;
newBook.Extension = txtExtension.Text;
newBook.LastAccessed = dtLastAccessed.Value;
newBook.Created = dtCreated.Value;
newBook.FilePath = txtFilePath.Text;
newBook.FileSize = txtFileSize.Text;
newBook.Title = txtTitle.Text;
newBook.Author = txtAuthor.Text;
newBook.Publisher = txtPublisher.Text;
newBook.Price = txtPrice.Text;
newBook.ISBN = txtISBN.Text;
newBook.PublishDate = dtDatePublished.Value;
newBook.Category = txtCategory.Text;
if (ebooks == null)
ebooks = new List<Document>();
ebooks.Add(newBook);
existingSpace.BookList = ebooks;
}
}
spaces.WriteToDataStore(_jsonPath);
PopulateStorageSpacesList();
MessageBox.Show("Book added");
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
}
Lastly, as a matter of housekeeping, the ImportBooks
form contains the following code for displaying and enabling controls based on the button click events of btnCancelNewStorageSpace
and btnAddNewStorageSpace
buttons:
private void btnCancelNewStorageSpaceSave_Click(object sender, EventArgs e)
{
txtNewStorageSpaceName.Clear();
txtNewStorageSpaceName.Visible = false;
lblStorageSpaceDescription.Visible = false;
txtStorageSpaceDescription.ReadOnly = true;
txtStorageSpaceDescription.Clear();
btnSaveNewStorageSpace.Visible = false;
btnCancelNewStorageSpaceSave.Visible = false;
dlVirtualStorageSpaces.Enabled = true;
btnAddNewStorageSpace.Enabled = true;
}
private void btnAddNewStorageSpace_Click(object sender, EventArgs e)
{
txtNewStorageSpaceName.Visible = true;
lblStorageSpaceDescription.Visible = true;
txtStorageSpaceDescription.ReadOnly = false;
btnSaveNewStorageSpace.Visible = true;
btnCancelNewStorageSpaceSave.Visible = true;
dlVirtualStorageSpaces.Enabled = false;
btnAddNewStorageSpace.Enabled = false;
}
All that remains now is for us to complete the code in the Form1.cs
form, which is the start-up form.