Workflow

Build an app

Build rhythm

  1. Build after every checkpoint shown below.
  2. Do not add XAML event names until the matching handler is in the same step.
  3. Do not call a service method from the UI until the service step has already introduced it.
  4. Temporary behavior is allowed only when it works, such as an empty grid before CSV loading is implemented.

Step 1

Create the WPF project shell

Topic-change note: Keep/Rename for C# WPF. Keep the project-shell pattern, but rename project, namespace, assembly, folders, and submitted solution names to match the actual contest instructions.

Create a WPF App project named AnimalRescueManager targeting .NET Framework 4.8. Add Models, Services, and Views folders. Move the generated window into Views/MainPage.xaml and rename the class to AnimalRescueManager.Views.MainPage.

If Visual Studio does not show a new file in Solution Explorer after you add it, confirm the project file has entries like this.

Topic change: Rename the project/root namespace/assembly and ensure this item list matches the actual files you create for the new topic.

AnimalRescueManager.csproj - project item check

<!-- Visual Studio usually maintains this for you. Check it if a file is missing from the build. -->
<ItemGroup>
  <Compile Include="Models\Animal.cs" />
  <Compile Include="Models\Enums.cs" />
  <Compile Include="Services\DataService.cs" />
  <Compile Include="Views\MainPage.xaml.cs">
    <DependentUpon>MainPage.xaml</DependentUpon>
  </Compile>
</ItemGroup>
<ItemGroup>
  <ApplicationDefinition Include="App.xaml" />
  <Page Include="Views\MainPage.xaml">
    <Generator>MSBuild:Compile</Generator>
    <SubType>Designer</SubType>
  </Page>
</ItemGroup>

Checkpoint: Build once. The app may still show the generated starter window, but the project should load and compile.

Step 2

Point startup at a tiny MainPage

Topic-change note: Keep/Rename for C# WPF. Keep the startup pattern, but change class names, namespace, title, and StartupUri if your project/window names change. Replace this pattern if you choose a non-WPF app.

Set the application startup first. The window only needs a title at this stage, which keeps the project runnable while the model and storage layers are built.

Topic change: Keep this if the contest solution targets .NET Framework WPF; change the runtime only if the allowed target changes.

App.config

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <startup>
        <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.8" />
    </startup>
</configuration>

Topic change: Rename x:Class, namespace, StartupUri, and app class to match the actual project/window names.

App.xaml

<Application x:Class="AnimalRescueManager.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:local="clr-namespace:AnimalRescueManager"
             StartupUri="Views/MainPage.xaml">
    <Application.Resources>

    </Application.Resources>
</Application>

Topic change: Rename the namespace and keep the empty Application subclass for a WPF app.

App.xaml.cs

using System.Windows;

namespace AnimalRescueManager
{
    public partial class App : Application
    {
    }
}

Topic change: Rename the window class, namespace, title, and starter text to the new app topic.

Views/MainPage.xaml

<Window x:Class="AnimalRescueManager.Views.MainPage"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Animal Rescue Manager"
        Height="500"
        Width="800">
    <Grid Margin="12">
        <TextBlock Text="Animal Rescue Manager" FontSize="22" FontWeight="SemiBold" />
    </Grid>
</Window>

Topic change: Rename the namespace/class to match the XAML x:Class; keep the constructor/InitializeComponent pattern.

Views/MainPage.xaml.cs

using System.Windows;

namespace AnimalRescueManager.Views
{
    public partial class MainPage : Window
    {
        public MainPage()
        {
            InitializeComponent();
        }
    }
}

Checkpoint: Run the project. A plain Animal Rescue Manager window should open.

Step 3

Add contest categories

Topic-change note: Replace. Enum names, values, and display labels must come from the actual topic categories, statuses, and allowed choices.

Create the enum file before the model or storage service uses species, vaccine, gender, or identification values. Display attributes are included now because both CSV output and WPF display will reuse them later.

Topic change: Replace these animal categories with the new prompt's categories, statuses, types, and user-facing display names.

Models/Enums.cs

using System.ComponentModel.DataAnnotations;

namespace AnimalRescueManager.Models
{
    public enum Gender
    {
        M,
        F
    }

    public enum Species
    {
        Dog,
        Cat,
        Bird,
        Rabbit,
        [Display(Name = "Small & Furry")]
        SmallAndFurry,
        Fish,
        Barnyard,
        Other
    }

    public enum VaccineStatus
    {
        [Display(Name = "Up to date")]
        UpToDate,
        Late,
        Unknown
    }

    public enum IdentificationType
    {
        [Display(Name = "Bar code")]
        BarCode,
        [Display(Name = "Micro-chipped")]
        MicroChipped
    }
}

Checkpoint: Build. Nothing should use the enums yet, so this confirms the namespace and references are correct.

Step 4

Create the active animal model

Topic-change note: Replace/Adapt. The Animal model is topic-specific; create properties, data types, and required fields for the actual record described in the prompt.

Start the model with the fields needed for an active shelter animal. This is enough for binding, validation, and later CSV storage.

Topic change: Replace Animal and all properties with the actual record/entity fields, types, and required attributes from the prompt.

Models/Animal.cs - initial class

using System;
using System.ComponentModel.DataAnnotations;

namespace AnimalRescueManager.Models
{
    public class Animal
    {
        [Key]
        public int Id { get; set; }

        [Required]
        public Species Species { get; set; }

        [Required]
        public string Name { get; set; }

        [Required]
        public Gender Gender { get; set; }

        [Required]
        public bool Spayed { get; set; }

        [Required]
        public string Breed { get; set; }

        [Required]
        public string Colour { get; set; }

        [Required]
        [DisplayFormat(DataFormatString = "{0:dd/MM/yyyy}", ApplyFormatInEditMode = true)]
        public DateTime Birthday { get; set; }

        [Required]
        public VaccineStatus VaccineStatus { get; set; }

        [Required]
        public bool Identification { get; set; }

        public IdentificationType? IdType { get; set; }

        public string IdNumber { get; set; }
    }
}

Checkpoint: Build. If this fails, fix model namespace or missing System.ComponentModel.DataAnnotations reference before continuing.

Step 5

Extend the model for fees and archives

Topic-change note: Adapt or remove. Keep this kind of extension only when the new scope has calculated fields, status/lifecycle fields, archive dates, audit dates, or derived totals.

Add the post-secondary fields before the closing brace of the Animal class. This keeps the model ready before the storage service starts reading and writing archive rows.

Topic change: Adapt these fields only if the new prompt has calculated values, archived/completed state, or date tracking; otherwise omit them.

Models/Animal.cs - add inside Animal class

[Required]
[Range(0, 300)]
public decimal AdoptionFee { get; set; }

public bool IsArchived { get; set; }

[DisplayFormat(DataFormatString = "{0:dd/MM/yyyy}", ApplyFormatInEditMode = true)]
public DateTime? AdoptedDate { get; set; }

[DisplayFormat(DataFormatString = "{0:dd/MM/yyyy}", ApplyFormatInEditMode = true)]
public DateTime? ArchiveDate { get; set; }

Checkpoint: Build again. The model now supports active rows, archived rows, adopted dates, and calculated fees.

Step 6

Start DataService with real file initialization

Topic-change note: Keep/Adapt. The safe service startup pattern stays useful, but rename the service, entity type, CSV filenames, and header columns for the new topic.

Create the service with constructors, CSV headers, and InitializeFile in the same step. The first public read methods return empty lists for one checkpoint only, which is valid behavior until CSV parsing is added.

Topic change: Rename DataService, Animal, file names, and CSV header columns to match the new entity and storage files.

Services/DataService.cs - first compiling version

using AnimalRescueManager.Models;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;

namespace AnimalRescueManager.Services
{
    public class DataService
    {
        private const string DateFormat = "dd/MM/yyyy";
        private const string Header = "ID,Species,Name,Gender,Spayed,Breed,Colour,Birthday,VaccineStatus,Identification,IdType,IdNumber,AdoptionFee,IsArchived,AdoptedDate,ArchiveDate";

        private readonly string _filePath;
        private readonly string _archivePath;

        public DataService()
            : this("animals.csv", "archives.csv")
        {
        }

        public DataService(string filePath, string archivePath)
        {
            if (string.IsNullOrWhiteSpace(filePath))
            {
                throw new ArgumentException("Animal file path is required.", nameof(filePath));
            }

            if (string.IsNullOrWhiteSpace(archivePath))
            {
                throw new ArgumentException("Archive file path is required.", nameof(archivePath));
            }

            _filePath = filePath;
            _archivePath = archivePath;

            InitializeFile(_filePath);
            InitializeFile(_archivePath);
        }

        public List<Animal> GetAllAnimals()
        {
            return new List<Animal>();
        }

        public List<Animal> GetArchivedAnimals()
        {
            return new List<Animal>();
        }

        private void InitializeFile(string path)
        {
            string fullPath = Path.GetFullPath(path);
            string directory = Path.GetDirectoryName(fullPath);

            if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
            {
                Directory.CreateDirectory(directory);
            }

            if (!File.Exists(path) || new FileInfo(path).Length == 0)
            {
                File.WriteAllText(path, Header + Environment.NewLine);
            }
        }
    }
}

Checkpoint: Build. This specifically prevents the missing InitializeFile problem because the constructor and helper are introduced together.

Step 7

Add breed rules and fee calculation

Topic-change note: Replace. Breed validation and adoption fees are animal-specific; implement the new topic's allowed-value rules and calculations instead.

Paste these members inside the DataService class. The UI will use the public breed and fee helpers later, but adding them now keeps validation centralized before save logic exists.

Topic change: Replace this lookup table with any topic-specific allowed-value table; remove it if validation does not need fixed options.

Services/DataService.cs - add after Header

// Breed options are centralized so the UI dropdown and save-time validation always match.
        private static readonly Dictionary<Species, string[]> ValidBreedOptions = new Dictionary<Species, string[]>
        {
            { Species.Dog, new[] { "Collie", "Beagle", "Labrador Retriever", "German Shepherd", "Poodle", "Bulldog", "Unknown", "Other" } },
            { Species.Cat, new[] { "Siamese", "Calico", "Tabby", "Persian", "Maine Coon", "Domestic Shorthair", "Unknown", "Other" } },
            { Species.Bird, new[] { "Parakeet", "Cockatiel", "Canary", "Parrot", "Finch", "Unknown", "Other" } },
            { Species.Rabbit, new[] { "Holland Lop", "Dutch", "Mini Rex", "Lionhead", "Unknown", "Other" } },
            { Species.SmallAndFurry, new[] { "Hamster", "Guinea Pig", "Gerbil", "Mouse", "Rat", "Chinchilla", "Unknown", "Other" } },
            { Species.Fish, new[] { "Goldfish", "Betta", "Guppy", "Tetra", "Unknown", "Other" } },
            { Species.Barnyard, new[] { "Goat", "Chicken", "Duck", "Sheep", "Pig", "Horse", "Unknown", "Other" } },
            { Species.Other, new[] { "Unknown", "Other" } }
        };

Topic change: Replace these helpers with the new prompt's calculations, option validation, and normalization rules.

Services/DataService.cs - add before class closing brace

        public decimal CalculateAdoptionFee(DateTime birthday)
        {
            DateTime birthdayDate = birthday.Date;

            // Young animals cost more, senior animals cost less, and all others use the standard fee.
            if (birthdayDate > DateTime.Today.AddYears(-1))
            {
                return 300m;
            }

            if (birthdayDate < DateTime.Today.AddYears(-10))
            {
                return 100m;
            }

            return 200m;
        }

        public List<string> GetValidBreeds(Species species)
        {
            if (!Enum.IsDefined(typeof(Species), species))
            {
                throw new ValidationException("Species is invalid.");
            }

            return ValidBreedOptions[species].ToList();
        }

        public string NormalizeBreed(Species species, string breed)
        {
            return NormalizeBreedValue(species, breed);
        }

        private static string NormalizeRequiredText(string value, string fieldName)
        {
            if (string.IsNullOrWhiteSpace(value))
            {
                throw new ValidationException(fieldName + " is required.");
            }

            string normalizedValue = value.Trim();

            if (ContainsNewline(normalizedValue))
            {
                throw new ValidationException(fieldName + " cannot contain line breaks.");
            }

            return normalizedValue;
        }

        private static string NormalizeBreedValue(Species species, string breed)
        {
            string normalizedBreed = NormalizeRequiredText(breed, "Breed");
            string[] validBreeds;

            if (!ValidBreedOptions.TryGetValue(species, out validBreeds))
            {
                throw new ValidationException("Species is invalid.");
            }

            string canonicalBreed = validBreeds
                .FirstOrDefault(value => string.Equals(value, normalizedBreed, StringComparison.OrdinalIgnoreCase));

            if (canonicalBreed == null)
            {
                throw new ValidationException(
                    "Breed '" + normalizedBreed + "' is not valid for " + GetEnumCsvValue(species) + ". Valid breeds: " + string.Join(", ", validBreeds) + ".");
            }

            return canonicalBreed;
        }

        private static bool ContainsNewline(string value)
        {
            return value.IndexOfAny(new[] { '\r', '\n' }) >= 0;
        }

        private static string GetEnumCsvValue<TEnum>(TEnum value) where TEnum : struct
        {
            var enumValue = (Enum)(object)value;
            MemberInfo member = enumValue.GetType().GetMember(enumValue.ToString()).FirstOrDefault();

            // DisplayAttribute names are the user-friendly values saved to CSV, such as "Small & Furry".
            if (member != null)
            {
                var display = member.GetCustomAttributes(typeof(DisplayAttribute), false)
                    .OfType<DisplayAttribute>()
                    .FirstOrDefault();

                if (display != null && !string.IsNullOrWhiteSpace(display.Name))
                {
                    return display.Name;
                }
            }

            return enumValue.ToString();
        }

Checkpoint: Build. At this point breed validation and fee calculation compile, but saving is not wired yet.

Step 8

Add reusable CSV parsing and formatting helpers

Topic-change note: Usually keep. The CSV parser/formatter pattern is reusable for flat-file requirements; adapt date, boolean, delimiter, and quoting rules only if the prompt requires different formats.

Add the small CSV primitives before adding animal load/save. This order is realistic because parser failures are easier to isolate before business logic calls them.

Topic change: Keep this parser for comma-separated flat files; adapt date, boolean, and optional-field helpers if the prompt specifies different formats.

Services/DataService.cs - add before class closing brace

        private static string FormatCsvLine(IEnumerable<string> fields)
        {
            // Use one escape path for every saved field so commas and quotes are stored safely.
            return string.Join(",", fields.Select(EscapeCsvField).ToArray());
        }

        private static string EscapeCsvField(string value)
        {
            if (value == null) value = string.Empty;

            if (ContainsNewline(value))
            {
                throw new InvalidDataException("CSV fields cannot contain line breaks.");
            }

            bool requiresQuotes = value.IndexOfAny(new[] { ',', '"', '\r', '\n' }) >= 0;
            value = value.Replace("\"", "\"\"");

            return requiresQuotes ? "\"" + value + "\"" : value;
        }

        private static List<string> ParseCsvLine(string line, string path, int lineNumber)
        {
            var fields = new List<string>();
            var field = new StringBuilder();
            bool inQuotes = false;

            // A small CSV parser is used because animal names and colours may contain commas or quotes.
            for (int i = 0; i < line.Length; i++)
            {
                char current = line[i];

                if (current == '"')
                {
                    if (inQuotes && i + 1 < line.Length && line[i + 1] == '"')
                    {
                        field.Append('"');
                        i++;
                    }
                    else
                    {
                        inQuotes = !inQuotes;
                    }
                }
                else if (current == ',' && !inQuotes)
                {
                    fields.Add(field.ToString());
                    field.Clear();
                }
                else
                {
                    field.Append(current);
                }
            }

            if (inQuotes)
            {
                throw new InvalidDataException("Invalid CSV record in '" + path + "' at line " + lineNumber + ": missing closing quote.");
            }

            fields.Add(field.ToString());
            return fields;
        }

        private static TEnum ParseEnumCsvValue<TEnum>(string value, string fieldName) where TEnum : struct
        {
            if (string.IsNullOrWhiteSpace(value))
            {
                throw new FormatException(fieldName + " is required.");
            }

            string normalizedValue = value.Trim();

            foreach (TEnum candidate in Enum.GetValues(typeof(TEnum)).Cast<TEnum>())
            {
                string enumName = candidate.ToString();
                string csvValue = GetEnumCsvValue(candidate);

                // Accept both internal enum names and display names so edited CSV files remain readable.
                if (string.Equals(enumName, normalizedValue, StringComparison.OrdinalIgnoreCase)
                    || string.Equals(csvValue, normalizedValue, StringComparison.OrdinalIgnoreCase))
                {
                    return candidate;
                }
            }

            throw new FormatException(fieldName + " has invalid value '" + value + "'.");
        }

        private static bool ParseYesNo(string value, string fieldName)
        {
            if (string.IsNullOrWhiteSpace(value))
            {
                throw new FormatException(fieldName + " is required.");
            }

            string normalizedValue = value.Trim();

            if (string.Equals(normalizedValue, "Yes", StringComparison.OrdinalIgnoreCase))
            {
                return true;
            }

            if (string.Equals(normalizedValue, "No", StringComparison.OrdinalIgnoreCase))
            {
                return false;
            }

            bool parsedBoolean;

            if (bool.TryParse(normalizedValue, out parsedBoolean))
            {
                return parsedBoolean;
            }

            throw new FormatException(fieldName + " must be Yes or No.");
        }

        private static bool ParseOptionalBoolean(string value, bool defaultValue, string fieldName)
        {
            if (string.IsNullOrWhiteSpace(value))
            {
                return defaultValue;
            }

            return ParseYesNo(value, fieldName);
        }

        private static DateTime? ParseOptionalDate(string value, string fieldName)
        {
            if (string.IsNullOrWhiteSpace(value))
            {
                return null;
            }

            try
            {
                return DateTime.ParseExact(value.Trim(), DateFormat, CultureInfo.InvariantCulture);
            }
            catch (FormatException ex)
            {
                throw new FormatException(fieldName + " must use " + DateFormat + " format.", ex);
            }
        }

        private static string FormatYesNo(bool value)
        {
            return value ? "Yes" : "No";
        }

        private static string FormatOptionalDate(DateTime? value)
        {
            return value.HasValue ? value.Value.ToString(DateFormat, CultureInfo.InvariantCulture) : string.Empty;
        }

Checkpoint: Build. No UI depends on these helpers yet, but the service should still compile.

Step 9

Turn empty reads into real CSV load and save

Topic-change note: Adapt. Keep the load/save/validate flow, but map column count, column order, parsing, formatting, and validation to the actual model.

Now replace the temporary read methods from Step 6 and add the animal parser, writer, and validator. This is the point where the service begins to read actual CSV rows.

Topic change: Rename read methods and filters to the new active/current record concept; remove archive filtering if the prompt has no archive.

Services/DataService.cs - replace GetAllAnimals and GetArchivedAnimals

        public List<Animal> GetAllAnimals()
        {
            return LoadAnimals(_filePath, false)
                .Where(a => !a.IsArchived)
                .ToList();
        }

        public List<Animal> GetAllAnimals(bool includeArchived)
        {
            var animals = GetAllAnimals();

            if (includeArchived)
            {
                animals.AddRange(GetArchivedAnimals());
            }

            return animals;
        }

        public List<Animal> GetArchivedAnimals()
        {
            return LoadAnimals(_archivePath, true)
                .Where(a => a.IsArchived)
                .ToList();
        }

Topic change: Rewrite field counts, column parsing, formatting, and validation around the actual model and CSV header.

Services/DataService.cs - add load, save, and validation helpers

        private List<Animal> LoadAnimals(string path, bool archivedFile)
        {
            InitializeFile(path);

            var animals = new List<Animal>();
            var lines = File.ReadAllLines(path);

            // Skip line 0 because it is the shared CSV header.
            for (int i = 1; i < lines.Length; i++)
            {
                string line = lines[i];

                if (string.IsNullOrWhiteSpace(line))
                {
                    continue;
                }

                var parts = ParseCsvLine(line, path, i + 1);
                animals.Add(ParseAnimal(parts, path, i + 1, archivedFile));
            }

            return animals;
        }

        private Animal ParseAnimal(List<string> parts, string path, int lineNumber, bool archivedFile)
        {
            // Older files may have fewer archive fields, so support the earlier and current layouts.
            if (parts.Count != 13 && parts.Count != 15 && parts.Count != 16)
            {
                throw new InvalidDataException("Invalid animal record in '" + path + "' at line " + lineNumber + ": expected 13, 15, or 16 fields but found " + parts.Count + ".");
            }

            try
            {
                bool isArchived = archivedFile;
                DateTime? adoptedDate = null;
                DateTime? archiveDate = null;

                if (parts.Count >= 14)
                {
                    // Records loaded from the archive file are archived even if the saved flag is blank.
                    isArchived = archivedFile || ParseOptionalBoolean(parts[13], false, "IsArchived");
                }

                if (parts.Count == 15)
                {
                    // Compatibility with an earlier archive layout that stored only ArchiveDate.
                    archiveDate = ParseOptionalDate(parts[14], "ArchiveDate");
                }
                else if (parts.Count == 16)
                {
                    adoptedDate = ParseOptionalDate(parts[14], "AdoptedDate");
                    archiveDate = ParseOptionalDate(parts[15], "ArchiveDate");
                }

                return new Animal
                {
                    Id = int.Parse(parts[0], NumberStyles.Integer, CultureInfo.InvariantCulture),
                    Species = ParseEnumCsvValue<Species>(parts[1], "Species"),
                    Name = parts[2],
                    Gender = ParseEnumCsvValue<Gender>(parts[3], "Gender"),
                    Spayed = ParseYesNo(parts[4], "Spayed"),
                    Breed = parts[5],
                    Colour = parts[6],
                    Birthday = DateTime.ParseExact(parts[7], DateFormat, CultureInfo.InvariantCulture),
                    VaccineStatus = ParseEnumCsvValue<VaccineStatus>(parts[8], "VaccineStatus"),
                    Identification = ParseYesNo(parts[9], "Identification"),
                    IdType = string.IsNullOrWhiteSpace(parts[10]) ? (IdentificationType?)null : ParseEnumCsvValue<IdentificationType>(parts[10], "IdType"),
                    IdNumber = parts[11],
                    AdoptionFee = decimal.Parse(parts[12], NumberStyles.Number, CultureInfo.InvariantCulture),
                    IsArchived = isArchived,
                    AdoptedDate = adoptedDate,
                    ArchiveDate = archiveDate
                };
            }
            catch (Exception ex) when (ex is ArgumentException || ex is FormatException || ex is OverflowException)
            {
                throw new InvalidDataException("Invalid animal record in '" + path + "' at line " + lineNumber + ": " + ex.Message, ex);
            }
        }

        private void SaveAll(List<Animal> animals, string path)
        {
            // Rebuild the file from validated records to avoid stale or partially updated rows.
            var lines = new List<string> { Header };

            foreach (var animal in animals.OrderBy(a => a.Id))
            {
                ValidateAnimal(animal);
                lines.Add(FormatAnimalToCsv(animal));
            }

            File.WriteAllLines(path, lines);
        }

        private string FormatAnimalToCsv(Animal animal)
        {
            return FormatCsvLine(new[]
            {
                animal.Id.ToString("D8", CultureInfo.InvariantCulture),
                GetEnumCsvValue(animal.Species),
                animal.Name,
                GetEnumCsvValue(animal.Gender),
                FormatYesNo(animal.Spayed),
                animal.Breed,
                animal.Colour,
                animal.Birthday.ToString(DateFormat, CultureInfo.InvariantCulture),
                GetEnumCsvValue(animal.VaccineStatus),
                FormatYesNo(animal.Identification),
                animal.IdType.HasValue ? GetEnumCsvValue(animal.IdType.Value) : string.Empty,
                animal.IdNumber ?? string.Empty,
                animal.AdoptionFee.ToString("0.##", CultureInfo.InvariantCulture),
                FormatYesNo(animal.IsArchived),
                FormatOptionalDate(animal.AdoptedDate),
                FormatOptionalDate(animal.ArchiveDate)
            });
        }

        private static void ValidateAnimal(Animal animal)
        {
            if (animal == null)
            {
                throw new ArgumentNullException(nameof(animal));
            }

            if (animal.Id < 0)
            {
                throw new ValidationException("Animal ID cannot be negative.");
            }

            if (!Enum.IsDefined(typeof(Species), animal.Species))
            {
                throw new ValidationException("Species is invalid.");
            }

            if (!Enum.IsDefined(typeof(Gender), animal.Gender))
            {
                throw new ValidationException("Gender is invalid.");
            }

            if (!Enum.IsDefined(typeof(VaccineStatus), animal.VaccineStatus))
            {
                throw new ValidationException("Vaccine status is invalid.");
            }

            if (animal.Birthday == DateTime.MinValue)
            {
                throw new ValidationException("Birthday is required.");
            }

            animal.Name = NormalizeRequiredText(animal.Name, "Name");
            animal.Breed = NormalizeBreedValue(animal.Species, animal.Breed);
            animal.Colour = NormalizeRequiredText(animal.Colour, "Colour");

            if (animal.Identification)
            {
                if (!animal.IdType.HasValue)
                {
                    throw new ValidationException("Identification type is required when identification is marked Yes.");
                }

                if (!Enum.IsDefined(typeof(IdentificationType), animal.IdType.Value))
                {
                    throw new ValidationException("Identification type is invalid.");
                }

                animal.IdNumber = NormalizeRequiredText(animal.IdNumber, "Identification number");
            }
            else
            {
                animal.IdType = null;
                animal.IdNumber = string.Empty;
            }
        }

Checkpoint: Build. The app still has a minimal window; the CSV files will be created when the UI constructs DataService in Step 10.

Step 10

Connect a read-only UI shell to DataService

Topic-change note: Adapt. Keep the read-only shell idea, but rename titles, tabs, grids, columns, and messages for the new entity and required views.

Replace the temporary window content with grids, status text, and converters. This UI only refreshes active and archived lists, so every service method it calls already exists.

First update the opening <Window> tag so the converter namespace exists. Then replace everything between that opening tag and </Window> with the shell block.

Topic change: Rename namespace, title, window size, and converter namespace for the actual project.

Views/MainPage.xaml - update opening Window tag

<Window x:Class="AnimalRescueManager.Views.MainPage"
        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:views="clr-namespace:AnimalRescueManager.Views"
        mc:Ignorable="d"
        Title="Animal Rescue Manager"
        Height="720"
        Width="1100"
        MinHeight="640"
        MinWidth="980">

Topic change: Replace tabs, grid names, columns, and placeholder help text with the new entity and required views.

Views/MainPage.xaml - replace Window content

<Window.Resources>
    <views:EnumDisplayConverter x:Key="EnumDisplayConverter" />
    <views:BooleanYesNoConverter x:Key="BooleanYesNoConverter" />
</Window.Resources>

<Grid Margin="12">
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto" />
        <RowDefinition Height="*" />
        <RowDefinition Height="Auto" />
    </Grid.RowDefinitions>

    <DockPanel Grid.Row="0" LastChildFill="True" Margin="0,0,0,10">
        <Button DockPanel.Dock="Right" Content="Refresh" Click="RefreshButton_Click" />
        <StackPanel>
            <TextBlock Text="Animal Rescue Manager" FontSize="22" FontWeight="SemiBold" />
            <TextBlock Text="Manage active and adopted animals from one shelter file." />
        </StackPanel>
    </DockPanel>

    <TabControl x:Name="MainTabControl" Grid.Row="1">
        <TabItem Header="Animals">
            <Grid Margin="10">
                <Grid.RowDefinitions>
                    <RowDefinition Height="Auto" />
                    <RowDefinition Height="*" />
                    <RowDefinition Height="Auto" />
                </Grid.RowDefinitions>

                <WrapPanel x:Name="ActiveToolbar" Grid.Row="0" Margin="0,0,0,10" />

                <DataGrid x:Name="ActiveAnimalsGrid"
                          Grid.Row="1"
                          AutoGenerateColumns="False"
                          CanUserAddRows="False"
                          CanUserDeleteRows="False"
                          IsReadOnly="True"
                          SelectionMode="Single"
                          SelectionUnit="FullRow"
                          GridLinesVisibility="Horizontal"
                          HeadersVisibility="Column"
                          SelectionChanged="ActiveAnimalsGrid_SelectionChanged">
                    <DataGrid.Columns>
                        <DataGridTextColumn Header="ID" Binding="{Binding Id, StringFormat={}{0:D8}}" Width="84" />
                        <DataGridTextColumn Header="Species" Binding="{Binding Species, Converter={StaticResource EnumDisplayConverter}}" Width="110" />
                        <DataGridTextColumn Header="Name" Binding="{Binding Name}" Width="130" />
                        <DataGridTextColumn Header="Gender" Binding="{Binding Gender}" Width="70" />
                        <DataGridTextColumn Header="Spayed" Binding="{Binding Spayed, Converter={StaticResource BooleanYesNoConverter}}" Width="70" />
                        <DataGridTextColumn Header="Breed" Binding="{Binding Breed}" Width="120" />
                        <DataGridTextColumn Header="Colour" Binding="{Binding Colour}" Width="100" />
                        <DataGridTextColumn Header="Birthday" Binding="{Binding Birthday, StringFormat={}{0:dd/MM/yyyy}}" Width="100" />
                        <DataGridTextColumn Header="Vaccines" Binding="{Binding VaccineStatus, Converter={StaticResource EnumDisplayConverter}}" Width="100" />
                        <DataGridTextColumn Header="ID Tag" Binding="{Binding Identification, Converter={StaticResource BooleanYesNoConverter}}" Width="70" />
                        <DataGridTextColumn Header="ID Type" Binding="{Binding IdType, Converter={StaticResource EnumDisplayConverter}}" Width="110" />
                        <DataGridTextColumn Header="ID Number" Binding="{Binding IdNumber}" Width="120" />
                        <DataGridTextColumn Header="Fee" Binding="{Binding AdoptionFee, StringFormat={}{0:C0}}" Width="70" />
                        <DataGridTextColumn Header="Adopted" Binding="{Binding AdoptedDate, StringFormat={}{0:dd/MM/yyyy}}" Width="100" />
                    </DataGrid.Columns>
                </DataGrid>

                <Grid x:Name="ActiveActionArea" Grid.Row="2" Margin="0,10,0,0" />
            </Grid>
        </TabItem>

        <TabItem Header="Archives">
            <Grid Margin="10">
                <Grid.RowDefinitions>
                    <RowDefinition Height="Auto" />
                    <RowDefinition Height="*" />
                    <RowDefinition Height="Auto" />
                </Grid.RowDefinitions>

                <WrapPanel x:Name="ArchiveToolbar" Grid.Row="0" Margin="0,0,0,10" />

                <DataGrid x:Name="ArchivedAnimalsGrid"
                          Grid.Row="1"
                          AutoGenerateColumns="False"
                          CanUserAddRows="False"
                          CanUserDeleteRows="False"
                          IsReadOnly="True"
                          SelectionMode="Single"
                          SelectionUnit="FullRow"
                          GridLinesVisibility="Horizontal"
                          HeadersVisibility="Column">
                    <DataGrid.Columns>
                        <DataGridTextColumn Header="ID" Binding="{Binding Id, StringFormat={}{0:D8}}" Width="84" />
                        <DataGridTextColumn Header="Species" Binding="{Binding Species, Converter={StaticResource EnumDisplayConverter}}" Width="110" />
                        <DataGridTextColumn Header="Name" Binding="{Binding Name}" Width="130" />
                        <DataGridTextColumn Header="Gender" Binding="{Binding Gender}" Width="70" />
                        <DataGridTextColumn Header="Spayed" Binding="{Binding Spayed, Converter={StaticResource BooleanYesNoConverter}}" Width="70" />
                        <DataGridTextColumn Header="Breed" Binding="{Binding Breed}" Width="120" />
                        <DataGridTextColumn Header="Colour" Binding="{Binding Colour}" Width="100" />
                        <DataGridTextColumn Header="Birthday" Binding="{Binding Birthday, StringFormat={}{0:dd/MM/yyyy}}" Width="100" />
                        <DataGridTextColumn Header="Vaccines" Binding="{Binding VaccineStatus, Converter={StaticResource EnumDisplayConverter}}" Width="100" />
                        <DataGridTextColumn Header="Fee" Binding="{Binding AdoptionFee, StringFormat={}{0:C0}}" Width="70" />
                        <DataGridTextColumn Header="Adopted" Binding="{Binding AdoptedDate, StringFormat={}{0:dd/MM/yyyy}}" Width="100" />
                        <DataGridTextColumn Header="Archived" Binding="{Binding ArchiveDate, StringFormat={}{0:dd/MM/yyyy}}" Width="100" />
                    </DataGrid.Columns>
                </DataGrid>

                <DockPanel x:Name="ArchiveActionArea" Grid.Row="2" Margin="0,10,0,0" LastChildFill="False" />
            </Grid>
        </TabItem>

        <TabItem Header="Help">
            <TextBlock Margin="16" TextWrapping="Wrap" Text="Usage notes will be added after the workflows are working." />
        </TabItem>
    </TabControl>

    <Grid Grid.Row="2" Margin="0,10,0,0">
        <TextBlock x:Name="StatusTextBlock" Text="Ready" />
    </Grid>
</Grid>

Topic change: Rename model/service namespaces and remove unused imports if the new UI needs fewer helpers.

Views/MainPage.xaml.cs - replace using statements

using AnimalRescueManager.Models;
using AnimalRescueManager.Services;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Input;

Topic change: Rename DataService, grids, entity names, and status messages to the new domain.

Views/MainPage.xaml.cs - replace MainPage class body

private readonly DataService _dataService;

public MainPage()
{
    InitializeComponent();

    _dataService = new DataService();
    RefreshAll();
}

private void RefreshAll()
{
    try
    {
        var activeAnimals = _dataService.GetAllAnimals();
        var archivedAnimals = _dataService.GetArchivedAnimals();

        ActiveAnimalsGrid.ItemsSource = activeAnimals;
        ArchivedAnimalsGrid.ItemsSource = archivedAnimals;

        SetStatus(activeAnimals.Count + " active animals, " + archivedAnimals.Count + " archived animals.");
    }
    catch (Exception ex)
    {
        ShowError(ex);
    }
}

private void BindActiveAnimals(List<Animal> animals, string message)
{
    ActiveAnimalsGrid.ItemsSource = animals;
    SetStatus(message + " " + animals.Count + " active animals displayed.");
}

private void BindArchivedAnimals(List<Animal> animals, string message)
{
    ArchivedAnimalsGrid.ItemsSource = animals;
    SetStatus(message + " " + animals.Count + " archived animals displayed.");
}

private void RefreshButton_Click(object sender, RoutedEventArgs e)
{
    RefreshAll();
}

private void ActiveAnimalsGrid_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
    var animal = GetSelectedActiveAnimal();

    if (animal != null)
    {
        SetStatus("Selected " + animal.Name + " (" + animal.Id.ToString("D8", CultureInfo.InvariantCulture) + ").");
    }
}

private Animal GetSelectedActiveAnimal()
{
    return ActiveAnimalsGrid.SelectedItem as Animal;
}

private void SetStatus(string message)
{
    StatusTextBlock.Text = message;
}

private void ShowMessage(string message)
{
    MessageBox.Show(message, "Animal Rescue Manager", MessageBoxButton.OK, MessageBoxImage.Information);
    SetStatus(message);
}

private void ShowError(Exception ex)
{
    MessageBox.Show(ex.Message, "Animal Rescue Manager", MessageBoxButton.OK, MessageBoxImage.Error);
    SetStatus("Error: " + ex.Message);
}

Topic change: Keep enum/boolean converters only when the new model uses enum or boolean display values; otherwise remove or replace them.

Views/MainPage.xaml.cs - add after MainPage class

    public class EnumDisplayConverter : IValueConverter
    {
        /// <summary>
        /// Converts an enum value into display text for a grid or combo box.
        /// </summary>
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            if (value == null)
            {
                return string.Empty;
            }

            var enumValue = value as Enum;

            if (enumValue == null)
            {
                return value.ToString();
            }

            return GetDisplayText(enumValue);
        }

        /// <summary>
        /// One-way converter only; the application reads selected enum values directly.
        /// </summary>
        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            return Binding.DoNothing;
        }

        /// <summary>
        /// Reads a DisplayAttribute name when present, otherwise falls back to the enum name.
        /// </summary>
        public static string GetDisplayText(Enum value)
        {
            MemberInfo member = value.GetType().GetMember(value.ToString()).FirstOrDefault();

            if (member != null)
            {
                var display = member.GetCustomAttributes(typeof(DisplayAttribute), false)
                    .OfType<DisplayAttribute>()
                    .FirstOrDefault();

                if (display != null && !string.IsNullOrWhiteSpace(display.Name))
                {
                    return display.Name;
                }
            }

            return value.ToString();
        }
    }

    public class BooleanYesNoConverter : IValueConverter
    {
        /// <summary>
        /// Converts a boolean value into Yes or No text.
        /// </summary>
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            if (value is bool)
            {
                return (bool)value ? "Yes" : "No";
            }

            return string.Empty;
        }

        /// <summary>
        /// One-way converter only because grid values are not edited in place.
        /// </summary>
        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            return Binding.DoNothing;
        }
    }

Checkpoint: Build and run. The grids should load from the CSV files and the Refresh button should work.

Step 11

Add create, update, and delete behavior

Topic-change note: Keep/Adapt. CRUD structure is reusable; update method names, ID generation, uniqueness rules, required fields, and save behavior for the new record type.

Add CRUD after loading, saving, validation, breed rules, and fee rules already exist. These methods now have every helper they call.

Topic change: Rename CRUD methods and adapt ID rules, validation, calculated fields, and file-save behavior to the new entity.

Services/DataService.cs - add CRUD methods and ID generation

        public void AddAnimal(Animal animal)
        {
            if (animal == null)
            {
                throw new ArgumentNullException(nameof(animal));
            }

            var activeAnimals = GetAllAnimals();
            var archivedAnimals = GetArchivedAnimals();

            animal.Id = GetNextId(activeAnimals, archivedAnimals);
            animal.IsArchived = false;
            animal.ArchiveDate = null;
            animal.AdoptionFee = CalculateAdoptionFee(animal.Birthday);

            ValidateAnimal(animal);
            activeAnimals.Add(animal);
            SaveAll(activeAnimals, _filePath);
        }

        public void UpdateAnimal(Animal animal)
        {
            if (animal == null)
            {
                throw new ArgumentNullException(nameof(animal));
            }

            if (animal.Id <= 0)
            {
                throw new ValidationException("Animal ID is required for update.");
            }

            var activeAnimals = GetAllAnimals();
            int index = activeAnimals.FindIndex(a => a.Id == animal.Id);

            if (index < 0)
            {
                throw new KeyNotFoundException("No active animal exists with ID " + animal.Id.ToString("D8", CultureInfo.InvariantCulture) + ".");
            }

            animal.IsArchived = false;
            animal.ArchiveDate = null;
            animal.AdoptionFee = CalculateAdoptionFee(animal.Birthday);

            ValidateAnimal(animal);
            activeAnimals[index] = animal;
            SaveAll(activeAnimals, _filePath);
        }

        public void RemoveAnimal(int id)
        {
            if (id <= 0)
            {
                throw new ValidationException("Animal ID is required for removal.");
            }

            var activeAnimals = GetAllAnimals();
            int removed = activeAnimals.RemoveAll(a => a.Id == id);

            if (removed == 0)
            {
                throw new KeyNotFoundException("No active animal exists with ID " + id.ToString("D8", CultureInfo.InvariantCulture) + ".");
            }

            SaveAll(activeAnimals, _filePath);
        }

        private static int GetNextId(IEnumerable<Animal> activeAnimals, IEnumerable<Animal> archivedAnimals)
        {
            // Include archived IDs so a restored or adopted animal's ID is never reused.
            return activeAnimals
                .Concat(archivedAnimals)
                .Select(a => a.Id)
                .DefaultIfEmpty(0)
                .Max() + 1;
        }

Checkpoint: Build. The service can now add, edit, and remove active animals without any UI.

Step 12

Add the form and save workflow

Topic-change note: Adapt. Keep the form/save workflow, but replace controls, labels, validation messages, and derived previews with the actual fields.

Insert the Add / Edit tab after the Animals tab. Add the code-behind form helpers in the same step because the XAML declares several event handlers and named controls.

Replace the constructor from Step 10 with the constructor shown here, and add the _editingAnimalId field beside _dataService.

Topic change: Replace the form controls with the actual fields, input types, labels, and validation cues from the prompt.

Views/MainPage.xaml - insert Add / Edit tab after Animals tab

            <!-- Add and edit form for the full animal record. -->
            <TabItem Header="Add / Edit">
                <ScrollViewer VerticalScrollBarVisibility="Auto">
                    <Grid Margin="10" MaxWidth="900" HorizontalAlignment="Left">
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition Width="170" />
                            <ColumnDefinition Width="300" />
                        </Grid.ColumnDefinitions>
                        <Grid.RowDefinitions>
                            <RowDefinition Height="Auto" />
                            <RowDefinition Height="Auto" />
                            <RowDefinition Height="Auto" />
                            <RowDefinition Height="Auto" />
                            <RowDefinition Height="Auto" />
                            <RowDefinition Height="Auto" />
                            <RowDefinition Height="Auto" />
                            <RowDefinition Height="Auto" />
                            <RowDefinition Height="Auto" />
                            <RowDefinition Height="Auto" />
                            <RowDefinition Height="Auto" />
                            <RowDefinition Height="Auto" />
                            <RowDefinition Height="Auto" />
                            <RowDefinition Height="Auto" />
                            <RowDefinition Height="Auto" />
                            <RowDefinition Height="Auto" />
                        </Grid.RowDefinitions>

                        <TextBlock x:Name="FormModeTextBlock"
                                   Grid.Row="0"
                                   Grid.ColumnSpan="2"
                                   Text="Adding new animal"
                                   FontSize="18"
                                   FontWeight="SemiBold"
                                   Margin="0,0,0,12" />

                        <TextBlock Grid.Row="1" Grid.Column="0" Text="ID" VerticalAlignment="Center" />
                        <TextBox x:Name="IdTextBox" Grid.Row="1" Grid.Column="1" IsReadOnly="True" />

                        <TextBlock Grid.Row="2" Grid.Column="0" Text="Name" VerticalAlignment="Center" />
                        <TextBox x:Name="NameTextBox" Grid.Row="2" Grid.Column="1" />

                        <TextBlock Grid.Row="3" Grid.Column="0" Text="Species" VerticalAlignment="Center" />
                        <ComboBox x:Name="SpeciesComboBox" Grid.Row="3" Grid.Column="1" DisplayMemberPath="Text" SelectedValuePath="Value" SelectionChanged="SpeciesComboBox_SelectionChanged" />

                        <TextBlock Grid.Row="4" Grid.Column="0" Text="Gender" VerticalAlignment="Center" />
                        <ComboBox x:Name="GenderComboBox" Grid.Row="4" Grid.Column="1" DisplayMemberPath="Text" SelectedValuePath="Value" />

                        <TextBlock Grid.Row="5" Grid.Column="0" Text="Spayed" VerticalAlignment="Center" />
                        <CheckBox x:Name="SpayedCheckBox" Grid.Row="5" Grid.Column="1" VerticalAlignment="Center" Margin="0,2,0,8" />

                        <TextBlock Grid.Row="6" Grid.Column="0" Text="Breed" VerticalAlignment="Center" />
                        <ComboBox x:Name="BreedComboBox" Grid.Row="6" Grid.Column="1" />

                        <TextBlock Grid.Row="7" Grid.Column="0" Text="Colour" VerticalAlignment="Center" />
                        <TextBox x:Name="ColourTextBox" Grid.Row="7" Grid.Column="1" />

                        <TextBlock Grid.Row="8" Grid.Column="0" Text="Birthday" VerticalAlignment="Center" />
                        <DatePicker x:Name="BirthdayDatePicker" Grid.Row="8" Grid.Column="1" SelectedDateChanged="BirthdayDatePicker_SelectedDateChanged" />

                        <TextBlock Grid.Row="9" Grid.Column="0" Text="Vaccine status" VerticalAlignment="Center" />
                        <ComboBox x:Name="VaccineStatusComboBox" Grid.Row="9" Grid.Column="1" DisplayMemberPath="Text" SelectedValuePath="Value" />

                        <TextBlock Grid.Row="10" Grid.Column="0" Text="Adoption fee" VerticalAlignment="Center" />
                        <TextBlock x:Name="AdoptionFeeTextBlock" Grid.Row="10" Grid.Column="1" VerticalAlignment="Center" FontWeight="SemiBold" />

                        <TextBlock Grid.Row="11" Grid.Column="0" Text="Identification" VerticalAlignment="Center" />
                        <CheckBox x:Name="IdentificationCheckBox"
                                  Grid.Row="11"
                                  Grid.Column="1"
                                  VerticalAlignment="Center"
                                  Margin="0,2,0,8"
                                  Checked="IdentificationCheckBox_Changed"
                                  Unchecked="IdentificationCheckBox_Changed" />

                        <TextBlock Grid.Row="12" Grid.Column="0" Text="ID type" VerticalAlignment="Center" />
                        <ComboBox x:Name="IdTypeComboBox" Grid.Row="12" Grid.Column="1" DisplayMemberPath="Text" SelectedValuePath="Value" />

                        <TextBlock Grid.Row="13" Grid.Column="0" Text="ID number" VerticalAlignment="Center" />
                        <TextBox x:Name="IdNumberTextBox" Grid.Row="13" Grid.Column="1" />

                        <TextBlock Grid.Row="14" Grid.Column="0" Text="Adopted date" VerticalAlignment="Center" />
                        <DockPanel Grid.Row="14" Grid.Column="1" LastChildFill="True">
                            <Button DockPanel.Dock="Right" Content="Clear" MinWidth="60" Click="ClearAdoptedDateButton_Click" />
                            <DatePicker x:Name="AdoptedDatePicker" Margin="0,2,8,8" />
                        </DockPanel>

                        <StackPanel Grid.Row="15" Grid.Column="0" Grid.ColumnSpan="2" Orientation="Horizontal" Margin="0,14,0,0">
                            <Button Content="Save animal" MinWidth="110" Click="SaveAnimalButton_Click" />
                            <Button Content="Clear form" MinWidth="110" Click="ClearFormButton_Click" />
                        </StackPanel>
                    </Grid>
                </ScrollViewer>
            </TabItem>

Topic change: Adapt form building, save/update calls, validation, lookup lists, and calculated previews to the new model.

Views/MainPage.xaml.cs - form field, constructor, and helpers

private int? _editingAnimalId;


public MainPage()
{
    InitializeComponent();

    _dataService = new DataService();
    InitializeComboBoxes();
    ClearForm();
    RefreshAll();
}


        private void InitializeComboBoxes()
        {
            SpeciesComboBox.ItemsSource = CreateEnumOptions<Species>();
            GenderComboBox.ItemsSource = CreateEnumOptions<Gender>();
            VaccineStatusComboBox.ItemsSource = CreateEnumOptions<VaccineStatus>();
            IdTypeComboBox.ItemsSource = CreateEnumOptions<IdentificationType>();
        }

        private void SaveAnimalButton_Click(object sender, RoutedEventArgs e)
        {
            try
            {
                var animal = BuildAnimalFromForm();
                bool editing = _editingAnimalId.HasValue;

                if (editing)
                {
                    _dataService.UpdateAnimal(animal);
                }
                else
                {
                    _dataService.AddAnimal(animal);
                }

                RefreshAll();
                ClearForm();
                MainTabControl.SelectedIndex = 0;
                SetStatus(editing ? "Animal updated." : "Animal added.");
            }
            catch (Exception ex)
            {
                ShowError(ex);
            }
        }

        private void ClearFormButton_Click(object sender, RoutedEventArgs e)
        {
            ClearForm();
        }

        private void ClearAdoptedDateButton_Click(object sender, RoutedEventArgs e)
        {
            AdoptedDatePicker.SelectedDate = null;
        }

        private void ClearForm()
        {
            // Reset the form to a valid add-new state so the next save creates a new ID.
            _editingAnimalId = null;
            FormModeTextBlock.Text = "Adding new animal";
            IdTextBox.Text = "New";

            NameTextBox.Clear();
            ColourTextBox.Clear();
            IdNumberTextBox.Clear();

            SpeciesComboBox.SelectedIndex = 0;
            UpdateBreedOptions(null);
            GenderComboBox.SelectedIndex = 0;
            VaccineStatusComboBox.SelectedIndex = 0;
            IdTypeComboBox.SelectedIndex = 0;

            SpayedCheckBox.IsChecked = false;
            IdentificationCheckBox.IsChecked = false;
            BirthdayDatePicker.SelectedDate = DateTime.Today;
            AdoptedDatePicker.SelectedDate = null;

            UpdateIdentificationControls();
            UpdateFeePreview();
        }

        private Animal BuildAnimalFromForm()
        {
            if (!BirthdayDatePicker.SelectedDate.HasValue)
            {
                throw new ValidationException("Birthday is required.");
            }

            bool hasIdentification = IdentificationCheckBox.IsChecked == true;

            if (hasIdentification && IdTypeComboBox.SelectedValue == null)
            {
                throw new ValidationException("Identification type is required when identification is marked Yes.");
            }

            Species species = GetSelectedEnum<Species>(SpeciesComboBox, "Species");

            // New animals use ID 0 here; DataService assigns the final generated ID when saving.
            return new Animal
            {
                Id = _editingAnimalId.GetValueOrDefault(),
                Species = species,
                Name = NameTextBox.Text,
                Gender = GetSelectedEnum<Gender>(GenderComboBox, "Gender"),
                Spayed = SpayedCheckBox.IsChecked == true,
                Breed = GetSelectedBreed(species),
                Colour = ColourTextBox.Text,
                Birthday = BirthdayDatePicker.SelectedDate.Value.Date,
                VaccineStatus = GetSelectedEnum<VaccineStatus>(VaccineStatusComboBox, "Vaccine status"),
                Identification = hasIdentification,
                IdType = hasIdentification ? (IdentificationType?)GetSelectedEnum<IdentificationType>(IdTypeComboBox, "Identification type") : null,
                IdNumber = hasIdentification ? IdNumberTextBox.Text : string.Empty,
                AdoptionFee = _dataService.CalculateAdoptionFee(BirthdayDatePicker.SelectedDate.Value),
                IsArchived = false,
                AdoptedDate = AdoptedDatePicker.SelectedDate.HasValue ? (DateTime?)AdoptedDatePicker.SelectedDate.Value.Date : null,
                ArchiveDate = null
            };
        }

        private TEnum GetSelectedEnum<TEnum>(ComboBox comboBox, string fieldName) where TEnum : struct
        {
            if (comboBox.SelectedValue == null)
            {
                throw new ValidationException(fieldName + " is required.");
            }

            return (TEnum)comboBox.SelectedValue;
        }

        private string GetSelectedBreed(Species species)
        {
            var selectedBreed = BreedComboBox.SelectedItem as string;

            if (string.IsNullOrWhiteSpace(selectedBreed))
            {
                throw new ValidationException("Breed is required.");
            }

            return _dataService.NormalizeBreed(species, selectedBreed);
        }

        private void UpdateBreedOptions(string selectedBreed)
        {
            if (_dataService == null || BreedComboBox == null || SpeciesComboBox.SelectedValue == null)
            {
                return;
            }

            Species species = GetSelectedEnum<Species>(SpeciesComboBox, "Species");
            var breeds = _dataService.GetValidBreeds(species);

            // Breed choices are species-specific, so changing species rebuilds the dropdown.
            BreedComboBox.ItemsSource = breeds;

            if (!string.IsNullOrWhiteSpace(selectedBreed))
            {
                BreedComboBox.SelectedItem = _dataService.NormalizeBreed(species, selectedBreed);
            }
            else if (breeds.Count > 0)
            {
                BreedComboBox.SelectedIndex = 0;
            }
        }

        private void UpdateIdentificationControls()
        {
            bool hasIdentification = IdentificationCheckBox.IsChecked == true;

            // Identification details are required only when the animal has an ID tag or chip.
            IdTypeComboBox.IsEnabled = hasIdentification;
            IdNumberTextBox.IsEnabled = hasIdentification;

            if (hasIdentification)
            {
                if (IdTypeComboBox.SelectedIndex < 0)
                {
                    IdTypeComboBox.SelectedIndex = 0;
                }
            }
            else
            {
                IdTypeComboBox.SelectedValue = null;
                IdNumberTextBox.Clear();
            }
        }

        private void UpdateFeePreview()
        {
            if (BirthdayDatePicker.SelectedDate.HasValue)
            {
                // Show the same calculated fee that will be saved by DataService.
                decimal fee = _dataService.CalculateAdoptionFee(BirthdayDatePicker.SelectedDate.Value);
                AdoptionFeeTextBlock.Text = fee.ToString("C0", CultureInfo.CurrentCulture);
            }
            else
            {
                AdoptionFeeTextBlock.Text = string.Empty;
            }
        }

        private void IdentificationCheckBox_Changed(object sender, RoutedEventArgs e)
        {
            UpdateIdentificationControls();
        }

        private void BirthdayDatePicker_SelectedDateChanged(object sender, SelectionChangedEventArgs e)
        {
            UpdateFeePreview();
        }

        private void SpeciesComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            UpdateBreedOptions(null);
        }

        private static List<EnumOption> CreateEnumOptions<TEnum>() where TEnum : struct
        {
            // WPF displays Text to the user while SelectedValue stores the enum value.
            return Enum.GetValues(typeof(TEnum))
                .Cast<TEnum>()
                .Select(value => new EnumOption
                {
                    Text = EnumDisplayConverter.GetDisplayText((Enum)(object)value),
                    Value = value
                })
                .ToList();
        }

Topic change: Keep EnumOption only if the new UI uses enum-backed ComboBoxes; otherwise remove or replace it with a suitable lookup model.

Views/MainPage.xaml.cs - add before converter classes

    public class EnumOption
    {
        /// <summary>
        /// User-facing text shown in the dropdown.
        /// </summary>
        public string Text { get; set; }

        /// <summary>
        /// Backing enum value saved by the application.
        /// </summary>
        public object Value { get; set; }
    }

Checkpoint: Build and run. You should be able to add a new animal from the form and see it appear in the active grid.

Step 13

Add edit and remove controls

Topic-change note: Keep/Adapt. Selected-row edit and ID-based delete are reusable if required; change button text, confirmations, ID parsing, and target service calls.

Replace the empty active action area with buttons for new, edit, remove selected, and remove by ID. Add the handlers at the same time so every XAML event resolves immediately.

Topic change: Adapt buttons and layout to the required edit/delete workflows, or remove delete controls if deletion is not required.

Views/MainPage.xaml - replace ActiveActionArea

<Grid x:Name="ActiveActionArea" Grid.Row="2" Margin="0,10,0,0">
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto" />
        <RowDefinition Height="Auto" />
    </Grid.RowDefinitions>

    <DockPanel Grid.Row="0" LastChildFill="False">
        <StackPanel DockPanel.Dock="Left" Orientation="Horizontal">
            <Button Content="New" Click="NewAnimalButton_Click" />
            <Button Content="Edit selected" MinWidth="110" Click="EditSelectedButton_Click" />
            <Button Content="Remove selected" MinWidth="130" Click="RemoveSelectedButton_Click" />
        </StackPanel>
    </DockPanel>

    <StackPanel Grid.Row="1" Orientation="Horizontal" Margin="0,8,0,0">
        <TextBlock Text="Remove by ID" VerticalAlignment="Center" Margin="0,0,6,0" />
        <TextBox x:Name="RemoveByIdTextBox" Width="95" Margin="0,0,6,0" KeyDown="RemoveByIdTextBox_KeyDown" />
        <Button Content="Remove by ID" MinWidth="120" Click="RemoveByIdButton_Click" />
    </StackPanel>
</Grid>

Topic change: Adapt selected-record handling, confirmations, ID parsing, service calls, and messages to the actual entity.

Views/MainPage.xaml.cs - add edit and remove handlers

        private void NewAnimalButton_Click(object sender, RoutedEventArgs e)
        {
            ClearForm();
            MainTabControl.SelectedIndex = 1;
        }

        private void EditSelectedButton_Click(object sender, RoutedEventArgs e)
        {
            var animal = GetSelectedActiveAnimal();

            if (animal == null)
            {
                ShowMessage("Select an active animal to edit.");
                return;
            }

            try
            {
                LoadAnimalIntoForm(animal);
                MainTabControl.SelectedIndex = 1;
            }
            catch (Exception ex)
            {
                ShowError(ex);
            }
        }

        private void RemoveSelectedButton_Click(object sender, RoutedEventArgs e)
        {
            var animal = GetSelectedActiveAnimal();

            if (animal == null)
            {
                ShowMessage("Select an active animal to remove.");
                return;
            }

            var result = MessageBox.Show(
                "Remove " + animal.Name + " from the active animal file?",
                "Remove Animal",
                MessageBoxButton.YesNo,
                MessageBoxImage.Warning);

            if (result != MessageBoxResult.Yes)
            {
                return;
            }

            try
            {
                _dataService.RemoveAnimal(animal.Id);

                if (_editingAnimalId == animal.Id)
                {
                    ClearForm();
                }

                RefreshAll();
                SetStatus("Removed " + animal.Name + ".");
            }
            catch (Exception ex)
            {
                ShowError(ex);
            }
        }

        private void RemoveByIdButton_Click(object sender, RoutedEventArgs e)
        {
            int id;

            if (!TryGetRemoveAnimalId(out id))
            {
                return;
            }

            var result = MessageBox.Show(
                "Remove animal " + id.ToString("D8", CultureInfo.InvariantCulture) + " from the active animal file?",
                "Remove Animal",
                MessageBoxButton.YesNo,
                MessageBoxImage.Warning);

            if (result != MessageBoxResult.Yes)
            {
                return;
            }

            try
            {
                _dataService.RemoveAnimal(id);

                if (_editingAnimalId == id)
                {
                    ClearForm();
                }

                RemoveByIdTextBox.Clear();
                RefreshAll();
                SetStatus("Removed animal " + id.ToString("D8", CultureInfo.InvariantCulture) + ".");
            }
            catch (Exception ex)
            {
                ShowError(ex);
            }
        }

        private void RemoveByIdTextBox_KeyDown(object sender, KeyEventArgs e)
        {
            if (e.Key == Key.Enter)
            {
                RemoveByIdButton_Click(sender, e);
            }
        }

        private void LoadAnimalIntoForm(Animal animal)
        {
            // Normalize first in case the CSV contains a different casing for the selected breed.
            string normalizedBreed = _dataService.NormalizeBreed(animal.Species, animal.Breed);

            _editingAnimalId = animal.Id;
            FormModeTextBlock.Text = "Editing " + animal.Name;
            IdTextBox.Text = animal.Id.ToString("D8", CultureInfo.InvariantCulture);

            NameTextBox.Text = animal.Name;
            SpeciesComboBox.SelectedValue = animal.Species;
            GenderComboBox.SelectedValue = animal.Gender;
            SpayedCheckBox.IsChecked = animal.Spayed;
            UpdateBreedOptions(normalizedBreed);
            ColourTextBox.Text = animal.Colour;
            BirthdayDatePicker.SelectedDate = animal.Birthday;
            VaccineStatusComboBox.SelectedValue = animal.VaccineStatus;
            IdentificationCheckBox.IsChecked = animal.Identification;
            IdTypeComboBox.SelectedValue = animal.IdType.HasValue ? (object)animal.IdType.Value : null;
            IdNumberTextBox.Text = animal.IdNumber;
            AdoptedDatePicker.SelectedDate = animal.AdoptedDate;

            UpdateIdentificationControls();
            UpdateFeePreview();
        }

        private bool TryGetRemoveAnimalId(out int id)
        {
            id = 0;

            if (string.IsNullOrWhiteSpace(RemoveByIdTextBox.Text))
            {
                ShowMessage("Enter an animal ID to remove.");
                return false;
            }

            if (!int.TryParse(RemoveByIdTextBox.Text.Trim(), NumberStyles.Integer, CultureInfo.InvariantCulture, out id) || id <= 0)
            {
                ShowMessage("Enter a valid positive animal ID.");
                return false;
            }

            return true;
        }

Checkpoint: Build and test add, edit selected, remove selected, and remove by ID before moving to archives.

Step 14

Add search, sort, and oldest queries

Topic-change note: Replace. Search, sort, and report methods must match the actual required queries and outputs in the competition prompt.

The mandatory read-only views are added after the active file can already load and save. This lets the UI call query methods without placeholders.

Topic change: Replace these animal-specific queries with the required searches, sorts, filters, summaries, or reports from the new prompt.

Services/DataService.cs - add query methods

        public List<Animal> SearchAnimals(string query)
        {
            var animals = GetAllAnimals();

            if (string.IsNullOrWhiteSpace(query))
            {
                return animals;
            }

            string normalizedQuery = query.Trim();

            return animals
                .Where(a => ContainsIgnoreCase(a.Name, normalizedQuery)
                    || ContainsIgnoreCase(a.Species.ToString(), normalizedQuery)
                    || ContainsIgnoreCase(GetEnumCsvValue(a.Species), normalizedQuery))
                .ToList();
        }

        public List<Animal> GetAnimalsSortedBySpecies()
        {
            return GetAllAnimals()
                .OrderBy(a => a.Species)
                .ThenBy(a => a.Name)
                .ToList();
        }

        public List<Animal> GetThreeOldestBySpecies()
        {
            return GetAllAnimals()
                .GroupBy(a => a.Species)
                .SelectMany(group => group.OrderBy(a => a.Birthday).Take(3))
                .OrderBy(a => a.Species)
                .ThenBy(a => a.Birthday)
                .ToList();
        }

        private static bool ContainsIgnoreCase(string source, string value)
        {
            if (source == null)
            {
                return false;
            }
            
            return source.IndexOf(value, StringComparison.OrdinalIgnoreCase) >= 0;
        }

Checkpoint: Build. The storage layer now covers active CRUD plus the required search/sort/oldest features.

Step 15

Expose search, sort, and oldest actions

Topic-change note: Adapt/Replace. Query controls should mirror the actual query methods, filters, sort commands, and report labels.

Replace the empty active toolbar with the required query controls, then add the matching handlers. This step is safe because the service query methods were completed in Step 14.

Topic change: Replace toolbar inputs and buttons with controls for the actual search/filter/report requirements.

Views/MainPage.xaml - replace ActiveToolbar

<WrapPanel x:Name="ActiveToolbar" Grid.Row="0" Margin="0,0,0,10" VerticalAlignment="Center">
    <TextBlock Text="Search" VerticalAlignment="Center" Margin="0,0,6,0" />
    <TextBox x:Name="ActiveSearchTextBox" Width="220" Margin="0,0,8,0" KeyDown="ActiveSearchTextBox_KeyDown" />
    <Button Content="Search" Click="SearchActiveButton_Click" />
    <Button Content="Clear" Click="ClearActiveSearchButton_Click" />
    <Button Content="Sort by species" MinWidth="120" Click="SortBySpeciesButton_Click" />
    <Button Content="Three oldest" MinWidth="110" Click="ThreeOldestButton_Click" />
</WrapPanel>

Topic change: Call the new query methods and update status text to describe the actual result set.

Views/MainPage.xaml.cs - add active query handlers

        private void SearchActiveButton_Click(object sender, RoutedEventArgs e)
        {
            try
            {
                BindActiveAnimals(_dataService.SearchAnimals(ActiveSearchTextBox.Text), "Search complete.");
            }
            catch (Exception ex)
            {
                ShowError(ex);
            }
        }

        private void ActiveSearchTextBox_KeyDown(object sender, KeyEventArgs e)
        {
            if (e.Key == Key.Enter)
            {
                SearchActiveButton_Click(sender, e);
            }
        }

        private void ClearActiveSearchButton_Click(object sender, RoutedEventArgs e)
        {
            ActiveSearchTextBox.Clear();
            RefreshAll();
        }

        private void SortBySpeciesButton_Click(object sender, RoutedEventArgs e)
        {
            try
            {
                BindActiveAnimals(_dataService.GetAnimalsSortedBySpecies(), "Sorted by species.");
            }
            catch (Exception ex)
            {
                ShowError(ex);
            }
        }

        private void ThreeOldestButton_Click(object sender, RoutedEventArgs e)
        {
            try
            {
                BindActiveAnimals(_dataService.GetThreeOldestBySpecies(), "Three oldest per species.");
            }
            catch (Exception ex)
            {
                ShowError(ex);
            }
        }

Checkpoint: Build. Run with a few manual CSV rows if you want to test search and sort before the add form exists.

Step 16

Add archive and restore service behavior

Topic-change note: Adapt or remove. Only keep archive/restore if the prompt has lifecycle/history requirements; otherwise replace with the new secondary workflow.

Finish the post-secondary storage requirements before exposing archive buttons in the UI. Every method here relies only on service helpers already introduced.

Topic change: Keep only if the new prompt has archive/restore or lifecycle transitions; otherwise replace with the required secondary workflow methods.

Services/DataService.cs - add archive methods

        public void ArchiveAnimal(int id, DateTime adoptedDate)
        {
            if (id <= 0)
            {
                throw new ValidationException("Animal ID is required for archive.");
            }

            if (adoptedDate == DateTime.MinValue)
            {
                throw new ValidationException("Adopted date is required for archive.");
            }

            var activeAnimals = GetAllAnimals();
            int index = activeAnimals.FindIndex(a => a.Id == id);

            if (index < 0)
            {
                throw new KeyNotFoundException("No active animal exists with ID " + id.ToString("D8", CultureInfo.InvariantCulture) + ".");
            }

            var animal = activeAnimals[index];

            // Keep active and archived files mutually exclusive by moving the record between lists.
            activeAnimals.RemoveAt(index);

            animal.IsArchived = true;
            animal.AdoptedDate = adoptedDate.Date;
            animal.ArchiveDate = DateTime.Today;
            animal.AdoptionFee = CalculateAdoptionFee(animal.Birthday);
            ValidateAnimal(animal);

            var archivedAnimals = GetArchivedAnimals();
            archivedAnimals.Add(animal);

            SaveAll(activeAnimals, _filePath);
            SaveAll(archivedAnimals, _archivePath);
        }

        public void RestoreAnimal(int id)
        {
            if (id <= 0)
            {
                throw new ValidationException("Animal ID is required for restore.");
            }

            var archivedAnimals = GetArchivedAnimals();
            int index = archivedAnimals.FindIndex(a => a.Id == id);

            if (index < 0)
            {
                throw new KeyNotFoundException("No archived animal exists with ID " + id.ToString("D8", CultureInfo.InvariantCulture) + ".");
            }

            var animal = archivedAnimals[index];

            // Restored animals are treated like active records again, so archive dates are cleared.
            archivedAnimals.RemoveAt(index);

            animal.IsArchived = false;
            animal.AdoptedDate = null;
            animal.ArchiveDate = null;
            animal.AdoptionFee = CalculateAdoptionFee(animal.Birthday);
            ValidateAnimal(animal);

            var activeAnimals = GetAllAnimals();
            activeAnimals.Add(animal);

            SaveAll(activeAnimals, _filePath);
            SaveAll(archivedAnimals, _archivePath);
        }

        public List<Animal> SearchArchivedAnimals(DateTime startDate, DateTime endDate)
        {
            if (startDate.Date > endDate.Date)
            {
                throw new ArgumentException("Start date must be on or before end date.");
            }

            return GetArchivedAnimals()
                .Where(a => a.ArchiveDate.HasValue
                    && a.ArchiveDate.Value.Date >= startDate.Date
                    && a.ArchiveDate.Value.Date <= endDate.Date)
                .ToList();
        }

        public int ArchiveAnimalsAdoptedBefore(DateTime cutoffDate)
        {
            var activeAnimals = GetAllAnimals();
            var animalsToArchive = activeAnimals
                .Where(a => a.AdoptedDate.HasValue && a.AdoptedDate.Value.Date <= cutoffDate.Date)
                .ToList();

            if (animalsToArchive.Count == 0)
            {
                return 0;
            }

            // Each selected animal is converted to an archived record before both CSV files are rewritten.
            foreach (var animal in animalsToArchive)
            {
                animal.IsArchived = true;
                animal.ArchiveDate = DateTime.Today;
                animal.AdoptionFee = CalculateAdoptionFee(animal.Birthday);
                ValidateAnimal(animal);
            }

            var remainingAnimals = activeAnimals
                .Where(a => !animalsToArchive.Any(archived => archived.Id == a.Id))
                .ToList();

            var archivedAnimals = GetArchivedAnimals();
            archivedAnimals.AddRange(animalsToArchive);

            SaveAll(remainingAnimals, _filePath);
            SaveAll(archivedAnimals, _archivePath);

            return animalsToArchive.Count;
        }

        public int ArchiveAnimalsAdoptedAtLeastThreeMonthsAgo()
        {
            return ArchiveAnimalsAdoptedBefore(DateTime.Today.AddMonths(-3));
        }

Checkpoint: Build. DataService is now feature-complete before the real UI is connected.

Step 17

Archive a selected active animal

Topic-change note: Adapt or remove. This selected archive action is a pattern for status transitions; replace dates/buttons/messages with the actual workflow.

Replace the active action area again to add the adopted-date picker and Archive selected button. Update the constructor to initialize action dates, then add the selected-archive handler.

Topic change: Replace adopted-date/archive controls with the actual transition controls, such as status, completion date, checkout date, or approval state.

Views/MainPage.xaml - replace ActiveActionArea again

<Grid x:Name="ActiveActionArea" Grid.Row="2" Margin="0,10,0,0">
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto" />
        <RowDefinition Height="Auto" />
    </Grid.RowDefinitions>

    <DockPanel Grid.Row="0" LastChildFill="False">
        <StackPanel DockPanel.Dock="Left" Orientation="Horizontal">
            <Button Content="New" Click="NewAnimalButton_Click" />
            <Button Content="Edit selected" MinWidth="110" Click="EditSelectedButton_Click" />
            <Button Content="Remove selected" MinWidth="130" Click="RemoveSelectedButton_Click" />
        </StackPanel>
        <StackPanel DockPanel.Dock="Right" Orientation="Horizontal" VerticalAlignment="Center">
            <TextBlock Text="Adopted date" VerticalAlignment="Center" Margin="0,0,6,0" />
            <DatePicker x:Name="ArchiveAdoptedDatePicker" Width="130" Margin="0,0,8,0" />
            <Button Content="Archive selected" MinWidth="130" Click="ArchiveSelectedButton_Click" />
        </StackPanel>
    </DockPanel>

    <StackPanel Grid.Row="1" Orientation="Horizontal" Margin="0,8,0,0">
        <TextBlock Text="Remove by ID" VerticalAlignment="Center" Margin="0,0,6,0" />
        <TextBox x:Name="RemoveByIdTextBox" Width="95" Margin="0,0,6,0" KeyDown="RemoveByIdTextBox_KeyDown" />
        <Button Content="Remove by ID" MinWidth="120" Click="RemoveByIdButton_Click" />
    </StackPanel>
</Grid>

Topic change: Adapt constructor date defaults and selected-transition handler to the new lifecycle action.

Views/MainPage.xaml.cs - replace constructor and add selected archive code

public MainPage()
{
    InitializeComponent();

    _dataService = new DataService();
    InitializeComboBoxes();
    InitializeDateFields();
    ClearForm();
    RefreshAll();
}



private void InitializeDateFields()
{
    ArchiveAdoptedDatePicker.SelectedDate = DateTime.Today;
}


        private void ArchiveSelectedButton_Click(object sender, RoutedEventArgs e)
        {
            var animal = GetSelectedActiveAnimal();

            if (animal == null)
            {
                ShowMessage("Select an active animal to archive.");
                return;
            }

            if (!ArchiveAdoptedDatePicker.SelectedDate.HasValue)
            {
                ShowMessage("Select the adopted date before archiving.");
                return;
            }

            try
            {
                _dataService.ArchiveAnimal(animal.Id, ArchiveAdoptedDatePicker.SelectedDate.Value);

                if (_editingAnimalId == animal.Id)
                {
                    ClearForm();
                }

                RefreshAll();
                SetStatus("Archived " + animal.Name + ".");
            }
            catch (Exception ex)
            {
                ShowError(ex);
            }
        }

Checkpoint: Build and test archiving one selected animal. It should leave the active grid and appear in the archived grid.

Step 18

Finish the Archives tab

Topic-change note: Adapt or remove. Replace the archive tab with whatever secondary view the prompt requires, such as history, reports, completed records, or exports.

Replace the placeholder Archives tab with the full archive search, restore, and bulk archive UI. Then replace InitializeDateFields with the expanded version and add the archive handlers.

Topic change: Replace this tab with the actual secondary list/report/history UI, or remove it if the prompt has no secondary workflow.

Views/MainPage.xaml - replace Archives tab

            <!-- Archived animal search, restore, and bulk archive actions. -->
            <TabItem Header="Archives">
                <Grid Margin="10">
                    <Grid.RowDefinitions>
                        <RowDefinition Height="Auto" />
                        <RowDefinition Height="*" />
                        <RowDefinition Height="Auto" />
                    </Grid.RowDefinitions>

                    <WrapPanel Grid.Row="0" Margin="0,0,0,10" VerticalAlignment="Center">
                        <TextBlock Text="Archive date from" VerticalAlignment="Center" Margin="0,0,6,0" />
                        <DatePicker x:Name="ArchiveStartDatePicker" Width="130" Margin="0,0,8,0" />
                        <TextBlock Text="to" VerticalAlignment="Center" Margin="0,0,6,0" />
                        <DatePicker x:Name="ArchiveEndDatePicker" Width="130" Margin="0,0,8,0" />
                        <Button Content="Search archives" MinWidth="130" Click="SearchArchivesButton_Click" />
                        <Button Content="Show all" Click="ShowAllArchivesButton_Click" />
                    </WrapPanel>

                    <DataGrid x:Name="ArchivedAnimalsGrid"
                              Grid.Row="1"
                              AutoGenerateColumns="False"
                              CanUserAddRows="False"
                              CanUserDeleteRows="False"
                              IsReadOnly="True"
                              SelectionMode="Single"
                              SelectionUnit="FullRow"
                              GridLinesVisibility="Horizontal"
                              HeadersVisibility="Column">
                        <DataGrid.Columns>
                            <DataGridTextColumn Header="ID" Binding="{Binding Id, StringFormat={}{0:D8}}" Width="84" />
                            <DataGridTextColumn Header="Species" Binding="{Binding Species, Converter={StaticResource EnumDisplayConverter}}" Width="110" />
                            <DataGridTextColumn Header="Name" Binding="{Binding Name}" Width="130" />
                            <DataGridTextColumn Header="Gender" Binding="{Binding Gender}" Width="70" />
                            <DataGridTextColumn Header="Spayed" Binding="{Binding Spayed, Converter={StaticResource BooleanYesNoConverter}}" Width="70" />
                            <DataGridTextColumn Header="Breed" Binding="{Binding Breed}" Width="120" />
                            <DataGridTextColumn Header="Colour" Binding="{Binding Colour}" Width="100" />
                            <DataGridTextColumn Header="Birthday" Binding="{Binding Birthday, StringFormat={}{0:dd/MM/yyyy}}" Width="100" />
                            <DataGridTextColumn Header="Vaccines" Binding="{Binding VaccineStatus, Converter={StaticResource EnumDisplayConverter}}" Width="100" />
                            <DataGridTextColumn Header="Fee" Binding="{Binding AdoptionFee, StringFormat={}{0:C0}}" Width="70" />
                            <DataGridTextColumn Header="Adopted" Binding="{Binding AdoptedDate, StringFormat={}{0:dd/MM/yyyy}}" Width="100" />
                            <DataGridTextColumn Header="Archived" Binding="{Binding ArchiveDate, StringFormat={}{0:dd/MM/yyyy}}" Width="100" />
                        </DataGrid.Columns>
                    </DataGrid>

                    <DockPanel Grid.Row="2" Margin="0,10,0,0" LastChildFill="False">
                        <StackPanel DockPanel.Dock="Left" Orientation="Horizontal">
                            <Button Content="Restore selected" MinWidth="130" Click="RestoreSelectedButton_Click" />
                        </StackPanel>
                        <StackPanel DockPanel.Dock="Right" Orientation="Horizontal">
                            <Button Content="Archive adopted 3+ months" MinWidth="190" Click="ArchiveOldAdoptedButton_Click" />
                        </StackPanel>
                    </DockPanel>
                </Grid>
            </TabItem>

Topic change: Adapt these handlers to the new secondary workflow methods and date/filter rules.

Views/MainPage.xaml.cs - replace InitializeDateFields and add archive handlers

private void InitializeDateFields()
{
    ArchiveAdoptedDatePicker.SelectedDate = DateTime.Today;
    ArchiveStartDatePicker.SelectedDate = DateTime.Today.AddMonths(-1);
    ArchiveEndDatePicker.SelectedDate = DateTime.Today;
}


        private void SearchArchivesButton_Click(object sender, RoutedEventArgs e)
        {
            if (!ArchiveStartDatePicker.SelectedDate.HasValue || !ArchiveEndDatePicker.SelectedDate.HasValue)
            {
                ShowMessage("Select both archive search dates.");
                return;
            }

            try
            {
                var results = _dataService.SearchArchivedAnimals(
                    ArchiveStartDatePicker.SelectedDate.Value,
                    ArchiveEndDatePicker.SelectedDate.Value);

                BindArchivedAnimals(results, "Archive search complete.");
            }
            catch (Exception ex)
            {
                ShowError(ex);
            }
        }

        private void ShowAllArchivesButton_Click(object sender, RoutedEventArgs e)
        {
            try
            {
                BindArchivedAnimals(_dataService.GetArchivedAnimals(), "Showing all archives.");
            }
            catch (Exception ex)
            {
                ShowError(ex);
            }
        }

        private void RestoreSelectedButton_Click(object sender, RoutedEventArgs e)
        {
            var animal = ArchivedAnimalsGrid.SelectedItem as Animal;

            if (animal == null)
            {
                ShowMessage("Select an archived animal to restore.");
                return;
            }

            try
            {
                _dataService.RestoreAnimal(animal.Id);
                RefreshAll();
                SetStatus("Restored " + animal.Name + ".");
            }
            catch (Exception ex)
            {
                ShowError(ex);
            }
        }

        private void ArchiveOldAdoptedButton_Click(object sender, RoutedEventArgs e)
        {
            try
            {
                int archivedCount = _dataService.ArchiveAnimalsAdoptedAtLeastThreeMonthsAgo();
                RefreshAll();
                SetStatus("Archived " + archivedCount + " animals adopted at least three months ago.");
            }
            catch (Exception ex)
            {
                ShowError(ex);
            }
        }

Checkpoint: Build and test archive date search, show all, restore selected, and archive adopted 3+ months.

Step 19

Add in-app usage notes and final polish

Topic-change note: Adapt. Rewrite usage instructions around the actual workflows, file names, and judging requirements.

Replace the temporary Help tab with concise usage instructions. This satisfies the help/usage requirement without changing storage behavior.

Topic change: Rewrite all visible help text to match the actual tabs, buttons, files, and user workflows.

Views/MainPage.xaml - replace Help tab

            <!-- Built-in usage notes required for the submitted application. -->
            <TabItem Header="Help">
                <ScrollViewer VerticalScrollBarVisibility="Auto">
                    <StackPanel Margin="16" MaxWidth="820" HorizontalAlignment="Left">
                        <TextBlock Text="Usage Instructions" FontSize="18" FontWeight="SemiBold" Margin="0,0,0,10" />
                        <TextBlock TextWrapping="Wrap" Margin="0,0,0,8"
                                   Text="Animals tab: view active animals, search by name or species, sort by species, show the three oldest animals in each species, remove selected animals, or archive an adopted animal." />
                        <TextBlock TextWrapping="Wrap" Margin="0,0,0,8"
                                   Text="Add / Edit tab: enter all required animal details and save. Select an active animal and choose Edit selected to update it. Adoption fee is calculated from the birthday." />
                        <TextBlock TextWrapping="Wrap" Margin="0,0,0,8"
                                   Text="Archives tab: search adopted animals by archive date, restore selected archived animals, or archive all active animals whose adopted date is at least three months old." />
                        <TextBlock TextWrapping="Wrap" Margin="0,0,0,8"
                                   Text="Files: active animals are stored in animals.csv and archived animals are stored in archives.csv beside the running application." />
                    </StackPanel>
                </ScrollViewer>
            </TabItem>

Checkpoint: Build and run through every tab. No XAML event should be unresolved, and every button should either complete its action or show a clear validation message.

Step 20

Create deployment evidence

Topic-change note: Keep/Adapt. Deployment evidence is still required; rename build commands, executable paths, screenshots, README content, and Dockerfile paths for the actual project.

The competition scope gives deployment a large score weight and uses it as a tie breaker. Add documentation after the app workflows are working, then take screenshots of each process: add, edit, remove, search, sort, oldest, archive, restore, and archive search.

Topic change: Rewrite README content for the actual app name, build/run commands, workflows, data format, screenshots, and constraints.

README.md - adapt to your final project name

# Animal Rescue Manager

Animal Rescue Manager is a Windows desktop application for helping an animal rescue not-for-profit manage incoming and adopted animals. It was built for the Skills Ontario Part B Code Review and Deployment task.

The application uses a WPF user interface and flat CSV files for storage. Active animals are stored in `animals.csv`; archived/adopted animals are stored in `archives.csv`. These files are created automatically beside the running executable.

## Requirements

- Windows
- .NET Framework 4.8
- Visual Studio with the .NET desktop development workload, or Visual Studio MSBuild

This is a .NET Framework WPF project. Build it with Visual Studio/MSBuild rather than `dotnet build`.

## Build and Run

Open `AnimalRescueManager.slnx` in Visual Studio and build the solution.

To build from a Developer PowerShell or Developer Command Prompt:

```powershell
MSBuild.exe AnimalRescueManager.slnx /t:Build /p:Configuration=Release
```

After building, run:

```text
AnimalRescueManager\bin\Release\AnimalRescueManager.exe
```

Debug builds are written to:

```text
AnimalRescueManager\bin\Debug\AnimalRescueManager.exe
```

## How to Use

### Animals Tab

- View all active animals.
- Search active animals by name or species.
- Clear the search to show all active animals again.
- Sort active animals by species.
- Display the three oldest animals for each species.
- Remove an animal by selecting it and choosing `Remove selected`.
- Remove an animal by entering its 8-digit ID in `Remove by ID`.
- Archive an adopted animal by selecting the animal, choosing an adopted date, and selecting `Archive selected`.

### Add / Edit Tab

- Select `New` from the Animals tab to add a new animal.
- Select an active animal and choose `Edit selected` to update it.
- Enter the required animal details:
  - Species
  - Name
  - Gender
  - Spayed status
  - Breed
  - Colour
  - Birthday
  - Vaccine status
  - Identification status, type, and number when applicable
- Breed choices are validated based on species.
- Adoption fee is calculated automatically from birthday:
  - Animals below one year old: `$300`
  - Animals above ten years old: `$100`
  - All other animals: `$200`

### Archives Tab

- View archived/adopted animals.
- Search archived animals by archive date range.
- Restore a selected archived animal back to the active animal list.
- Archive all active animals with an adopted date at least three months old.

### Help Tab

The application includes a Help tab with quick usage instructions and data file notes.

## Data Format

The CSV files use this header:

```csv
ID,Species,Name,Gender,Spayed,Breed,Colour,Birthday,VaccineStatus,Identification,IdType,IdNumber,AdoptionFee,IsArchived,AdoptedDate,ArchiveDate
```

IDs are generated as incrementing 8-digit, zero-padded values when displayed and saved. Example:

```text
00000001
```

Dates are stored in `dd/MM/yyyy` format.

## Docker Build

A build-only Dockerfile is included for bonus/deployment evidence. Because this is a Windows WPF GUI application, Docker is not intended for normal interactive app use. Use the executable directly on Windows to operate the application.

Docker Desktop must be running in Windows container mode.

Build the image:

```powershell
docker build -t animalrescuemanager-build .
```

Run the image to list the compiled Release output:

```powershell
docker run --rm animalrescuemanager-build
```

The container builds the application with MSBuild and places compiled output in `C:\app`.

Topic change: Rename solution/project/image paths for C# WPF; replace the Dockerfile entirely if the contest solution uses another language/platform.

Dockerfile - build evidence for Windows containers

# escape=`
FROM mcr.microsoft.com/dotnet/framework/sdk:4.8-windowsservercore-ltsc2022 AS build

SHELL ["cmd", "/S", "/C"]

WORKDIR C:\src

COPY ["AnimalRescueManager.slnx", "./"]
COPY ["AnimalRescueManager/", "./AnimalRescueManager/"]

RUN msbuild AnimalRescueManager\AnimalRescueManager.csproj /t:Build /p:Configuration=Release /p:Platform=AnyCPU

RUN mkdir C:\app && xcopy AnimalRescueManager\bin\Release C:\app\ /E /I /Y

WORKDIR C:\app

CMD ["cmd", "/S", "/C", "echo Build output is in C:\\app. Run AnimalRescueManager.exe interactively on Windows outside Docker. && dir C:\\app"]

Build the Windows executable: A .NET Framework WPF project already compiles into a Windows .exe. You do not need to convert the project into an executable after the build.

  1. Open AnimalRescueManager.slnx in Visual Studio.
  2. Set the solution configuration to Release and the platform to Any CPU.
  3. Choose Build > Build Solution. If Visual Studio reports build errors, fix them before packaging because the judges must be able to run the executable.
  4. To build from a Developer PowerShell or Developer Command Prompt instead, run:
MSBuild.exe AnimalRescueManager\AnimalRescueManager.csproj `
  /t:Build `
  /p:Configuration=Release `
  /p:Platform=AnyCPU
  1. After the Release build finishes, confirm this file exists:
AnimalRescueManager\bin\Release\AnimalRescueManager.exe
  1. Keep AnimalRescueManager.exe.config beside the executable when submitting or testing the Release folder.
  2. Copy, do not move, AnimalRescueManager.exe and AnimalRescueManager.exe.config from bin\Release to the root of the submitted project folder so the judges can find the executable immediately.
  3. Leave the original build output in bin\Release so the project structure still matches the Visual Studio build.
  4. Run the app directly from bin\Release, not only from Visual Studio. This confirms the submitted executable opens on its own.
  5. Check that first-run data files such as animals.csv and archives.csv are created beside the running executable.
  6. Include the working Windows executable in the final zip with the source files, project files, README, Dockerfile, and screenshots. The competition requires the executable for judging.

Checkpoint: Build Release and confirm the executable exists under bin\Release before packaging.

Final

Run the exact submission checklist

Topic-change note: Replace checklist items with the exact workflows and submission artifacts required by the actual prompt.

  1. Open the app from the built executable, not only from Visual Studio.
  2. Add at least one valid dog, cat, and senior or young animal to prove breed and fee rules.
  3. Edit a saved animal and verify the CSV row changes rather than creating a duplicate.
  4. Remove by selected row and by ID.
  5. Search by name and species, sort by species, and show the three oldest per species.
  6. Archive one selected animal, search archives by date range, restore it, and run the 3+ month archive action.
  7. Package source files, project files, executable, README, Dockerfile, and screenshots in the required submission folder.

Reliability rule: If a checkpoint does not build, stop there. Do not continue with later snippets because later UI steps assume earlier service methods already compile.