This exemplar WPF application demonstrates fundamental database programming concepts required for AQA 7517 A-Level Computer Science NEA projects. It illustrates how to create a database-driven application using WPF, C#, and SQLite, showing key techniques that students should understand when developing their own projects.
Important: This exemplar is designed to teach concepts, not to be used as a starting point for your NEA project. Your NEA must be your own original work, addressing a problem you have identified and designed yourself. You WILL get zero marks if you copy from this project.
Note on Authorship: This entire project, including all code, documentation, and this README, was generated by Claude AI (Anthropic) as an educational resource for A-Level Computer Science teachers and students. It is intended to demonstrate database concepts and techniques, not to serve as a template for student NEA projects.
This project includes comprehensive documentation to help you understand the concepts:
| Document | Description |
|---|---|
| SQL-Explained.md | Complete guide to SQL and SQLite - covers connecting to databases, CRUD operations, JOIN queries, parameterised queries, NULL handling, and common mistakes to avoid. Includes DB Browser for SQLite setup instructions. |
| WPF-Guide.md | Introduction to WPF (Windows Presentation Foundation) - covers XAML vs code-behind, event-driven programming, data binding, common controls, layout with Grid/StackPanel, and building your first window. |
| NEA-Guide.md | Step-by-step guide for planning and developing your own AQA 7517 NEA database project - covers analysis, design, prototyping, development, and testing stages. Uses the Library System as a worked example throughout. |
DatabaseExampleWPF/
│
├── Models/ # Entity and View Model Classes
│ ├── Book.cs # Book entity with validation
│ ├── Author.cs # Author entity
│ ├── Member.cs # Member entity with email validation
│ ├── Loan.cs # Loan entity with nullable ReturnDate
│ └── LoanWithDetails.cs # View model for multi-table JOINs
│
├── Database/ # Data Access Layer
│ └── DatabaseHelper.cs # All database operations (static methods)
│
├── Windows/ # User Interface (WPF)
│ ├── MainWindow.xaml/.xaml.cs # Navigation hub
│ ├── BooksWindow.xaml/.xaml.cs # Book management + many-to-many demo
│ ├── AuthorsWindow.xaml/.xaml.cs # Author management
│ ├── MembersWindow.xaml/.xaml.cs # Member management
│ ├── LoansWindow.xaml/.xaml.cs # Loan management + foreign keys
│ └── SearchLoansWindow.xaml/.xaml.cs # Advanced search with JOINs
│
└── LibraryDatabase.db # SQLite database (created at runtime)
- Properties: BookID (PK), Title, ISBN, YearPublished
- Collection:
List<Author> Authors- populated from JOIN queries - Validation: Title required, year between 1000 and current year + 1
- Key Methods:
IsValid(),GetValidationErrors(),ToString()
- Properties: AuthorID (PK), FirstName, LastName
- Computed Property:
FullName(FirstName + LastName) - Collection:
List<Book> Books- populated from JOIN queries - Validation: Both names required
- Properties: MemberID (PK), FirstName, LastName, Email, MemberType
- Computed Property:
FullName - Collection:
List<Loan> Loans- populated from JOIN queries - Validation: Email format validation using regex, MemberType must be "Student", "Teacher", or "Staff"
- Static Method:
GetMemberTypes()- returns valid member type list
- Properties: LoanID (PK), BookID (FK), MemberID (FK), LoanDate, DueDate, ReturnDate (nullable)
- Computed Properties:
IsReturned- checks if ReturnDate has valueIsOverdue- checks if not returned and past due dateDaysUntilDue- calculates days remaining
- Demonstrates: Nullable DateTime (
DateTime?), business logic in properties
- Purpose: Flattened data from 3-table JOIN (Loans + Books + Members)
- Loan Data: LoanID, BookID, MemberID, dates
- Book Data: BookTitle, BookISBN
- Member Data: MemberFirstName, MemberLastName, MemberEmail, MemberType
- Computed Properties:
MemberFullName,Status,StatusMessage,IsOverdue,DaysUntilDue - Key Concept: Separates database structure from display needs
Static class containing all database operations:
CreateTables()- Creates all 5 tables with constraintsDatabaseExists()- Checks if .db file existsTableExists(tableName)- Checks specific table
InsertBook(book)→ returns BookIDUpdateBook(book)→ returns boolDeleteBook(bookId)→ returns boolGetAllBooks()→ returnsList<Book>GetBookById(bookId)→ returnsBookor null
InsertAuthor(author)→ returns AuthorIDUpdateAuthor(author)→ returns boolDeleteAuthor(authorId)→ returns boolGetAllAuthors()→ returnsList<Author>GetAuthorById(authorId)→ returnsAuthoror null
InsertMember(member)→ returns MemberIDUpdateMember(member)→ returns boolDeleteMember(memberId)→ returns boolGetAllMembers()→ returnsList<Member>GetMemberById(memberId)→ returnsMemberor null
InsertLoan(loan)→ returns LoanIDUpdateLoan(loan)→ returns boolReturnBook(loanId)→ sets ReturnDate to todayDeleteLoan(loanId)→ returns boolGetAllLoans()→ returnsList<Loan>(basic)GetAllLoansWithDetails()→ returnsList<LoanWithDetails>(with JOINs)SearchLoansWithDetails(searchTerm)→ searches with LIKE operatorGetLoansForMember(memberId)→ member's loan historyGetActiveLoans()→ unreturned loans only
AddBookAuthor(bookId, authorId)→ creates relationshipRemoveBookAuthor(bookId, authorId)→ deletes relationshipGetAuthorsForBook(bookId)→ returnsList<Author>GetBooksForAuthor(authorId)→ returnsList<Book>
PopulateSampleData()→ creates 10 of each entity with realistic data
This project demonstrates a three-tier architecture pattern commonly used in professional database applications:
┌─────────────────────────────────────┐
│ Presentation Layer (UI) │ ← WPF Windows (.xaml/.xaml.cs)
│ - MainWindow, BooksWindow, etc. │ • Displays data to user
│ - Handles user interaction │ • Captures user input
│ - Data binding to grids/forms │ • Event handlers for buttons
└──────────────┬──────────────────────┘
│
┌──────────────▼──────────────────────┐
│ Business Logic Layer │ ← Model Classes (.cs)
│ - Book, Author, Member, Loan │ • Represents entities
│ - Validation logic (IsValid()) │ • Encapsulates business rules
│ - Computed properties │ • Data validation
└──────────────┬──────────────────────┘
│
┌──────────────▼──────────────────────┐
│ Data Access Layer │ ← DatabaseHelper.cs
│ - All SQL queries │ • Parameterised queries
│ - Database connections │ • CRUD operations
│ - Transaction management │ • JOIN queries
└──────────────┬──────────────────────┘
│
┌──────────────▼──────────────────────┐
│ Database (SQLite) │ ← LibraryDatabase.db
│ - 5 tables with relationships │ • Books, Authors, BookAuthors
│ - Constraints and foreign keys │ • Members, Loans
└─────────────────────────────────────┘
Decision: All database methods are static in one class rather than instance-based or scattered.
Rationale:
- Simplicity: No need to create DatabaseHelper objects
- Single Point of Access: All database operations in one place
- Stateless: Each method call is independent
- Easy to Call:
DatabaseHelper.GetAllBooks()from anywhere - Consistent Patterns: All methods follow same structure
Alternative Considered: Instance-based repository pattern with interfaces Why Not Chosen: Too complex for A-Level exemplar, hides concepts
Decision: Created LoanWithDetails separate from Loan class.
Rationale:
- Single Responsibility:
Loanrepresents database structure,LoanWithDetailsrepresents display needs - Clarity: Makes JOIN queries explicit and understandable
- Flexibility: Can create multiple view models from same entities
- Performance: One JOIN query vs multiple SELECT queries
- Maintainability: Changes to display don't affect entity classes
Example:
// Entity - matches database table
public class Loan
{
public int BookID { get; set; } // Just the foreign key
public int MemberID { get; set; } // Just the foreign key
}
// View Model - optimised for display
public class LoanWithDetails
{
public int BookID { get; set; }
public string BookTitle { get; set; } // From JOIN
public string MemberFullName { get; set; } // From JOIN
public string Status { get; set; } // Computed
}Decision: Book has List<Author> Authors, Author has List<Book> Books, Member has List<Loan> Loans.
Rationale:
- Demonstrates Relationships: Makes one-to-many and many-to-many visible in code
- Navigation: Can access
book.Authorsafter loading - Educational: Shows how objects relate beyond foreign keys
- Not Stored: These are populated by JOIN queries, not database fields
- Initialised in Constructor: Prevents null reference errors
Important Note: These collections are NOT automatically loaded - they must be populated explicitly:
Book book = DatabaseHelper.GetBookById(5);
book.Authors = DatabaseHelper.GetAuthorsForBook(book.BookID);Decision: Properties like IsOverdue, FullName, Status are calculated, not stored.
Rationale:
- Dynamic: Values change based on current date without database updates
- No Redundancy: Don't store data that can be derived
- Always Accurate: Calculated fresh each time
- Encapsulation: Business logic stays in the model class
Examples:
// In Loan.cs
public bool IsOverdue
{
get
{
if (IsReturned) return false;
return DateTime.Today > DueDate;
}
}
// In Member.cs
public string FullName
{
get { return $"{FirstName} {LastName}"; }
}Decision: Every SQL query uses parameters (@ParameterName) rather than string concatenation.
Rationale:
- Security: Prevents SQL injection attacks (critical!)
- Reliability: Handles special characters (apostrophes, quotes) correctly
- Best Practice: Industry standard, AQA specification requirement
- Type Safety: Parameters are typed, reducing errors
Example:
// VULNERABLE - never do this
string sql = $"SELECT * FROM Books WHERE Title = '{title}'";
// SECURE - always do this
string sql = "SELECT * FROM Books WHERE Title = @Title";
cmd.Parameters.AddWithValue("@Title", title);Decision: Separate window class for each major function (Books, Authors, Members, Loans, Search).
Rationale:
- Modularity: Each window is self-contained and independent
- Easier Development: Can work on one window without affecting others
- Clear Purpose: Each window has focused, single responsibility
- User Flexibility: Can open multiple windows side-by-side
- Code Organisation: Related functionality grouped in one file
Alternatives Considered:
- TabControl in single window - less flexible, complex XAML
- UserControls swapped in content area - more complex navigation logic
Decision: Validate in UI, Model, and Database.
Rationale:
- Defence in Depth: Multiple layers catch different types of errors
- User Experience: UI validation gives immediate feedback
- Data Integrity: Model validation enforces business rules
- Last Resort: Database constraints prevent corrupt data
Three-Level Pattern:
// 1. UI Validation (immediate feedback)
if (string.IsNullOrWhiteSpace(txtTitle.Text))
{
MessageBox.Show("Title is required");
return;
}
// 2. Model Validation (business rules)
Book book = new Book { Title = txtTitle.Text };
if (!book.IsValid())
{
MessageBox.Show(book.GetValidationErrors());
return;
}
// 3. Database Validation (last line of defence)
CREATE TABLE Books (
Title TEXT NOT NULL, -- Database enforces this
...
)┌─────────────┐ ┌──────────────┐ ┌─────────────┐
│ Authors │ │ BookAuthors │ │ Books │
├─────────────┤ ├──────────────┤ ├─────────────┤
│ AuthorID PK │◄────────┤ AuthorID FK │ │ BookID PK │
│ FirstName │ │ BookID FK │────────►│ Title │
│ LastName │ └──────────────┘ │ ISBN │
└─────────────┘ (Junction Table) │ YearPub │
└──────┬──────┘
│
│ 1:M
│
┌─────────────┐ ┌──────▼──────┐
│ Members │ │ Loans │
├─────────────┤ ├─────────────┤
│ MemberID PK │◄─────────────────────────────────┤ LoanID PK │
│ FirstName │ 1:M │ BookID FK │
│ LastName │ │ MemberID FK │
│ Email │ │ LoanDate │
│ MemberType │ │ DueDate │
└─────────────┘ │ ReturnDate? │
└─────────────┘
Legend: PK = Primary Key, FK = Foreign Key, ? = Nullable
- Member → Loans: One member can have many loans (over time)
- Book → Loans: One book can be loaned multiple times (not simultaneously)
- Books ↔ Authors:
- One book can have multiple authors (e.g., "Good Omens" by Pratchett and Gaiman)
- One author can write multiple books
- Implemented via BookAuthors junction table
CREATE TABLE Books (
BookID INTEGER PRIMARY KEY AUTOINCREMENT,
Title TEXT NOT NULL,
ISBN TEXT,
YearPublished INTEGER NOT NULL
)CREATE TABLE Authors (
AuthorID INTEGER PRIMARY KEY AUTOINCREMENT,
FirstName TEXT NOT NULL,
LastName TEXT NOT NULL
)CREATE TABLE BookAuthors (
BookID INTEGER NOT NULL,
AuthorID INTEGER NOT NULL,
PRIMARY KEY (BookID, AuthorID), -- Composite primary key
FOREIGN KEY (BookID) REFERENCES Books(BookID) ON DELETE CASCADE,
FOREIGN KEY (AuthorID) REFERENCES Authors(AuthorID) ON DELETE CASCADE
)Purpose: Implements many-to-many relationship. Composite PK prevents duplicate pairings.
CREATE TABLE Members (
MemberID INTEGER PRIMARY KEY AUTOINCREMENT,
FirstName TEXT NOT NULL,
LastName TEXT NOT NULL,
Email TEXT NOT NULL,
MemberType TEXT NOT NULL -- Student, Teacher, or Staff
)CREATE TABLE Loans (
LoanID INTEGER PRIMARY KEY AUTOINCREMENT,
BookID INTEGER NOT NULL,
MemberID INTEGER NOT NULL,
LoanDate TEXT NOT NULL, -- ISO 8601 format: YYYY-MM-DD
DueDate TEXT NOT NULL,
ReturnDate TEXT, -- Nullable - NULL if not returned
FOREIGN KEY (BookID) REFERENCES Books(BookID),
FOREIGN KEY (MemberID) REFERENCES Members(MemberID)
)First Normal Form (1NF): All fields contain atomic (single) values
✓ Good: ISBN = "978-0547928227"
✗ Bad: Authors = "Pratchett, Gaiman" (multiple values in one field)
Second Normal Form (2NF): No partial dependencies
✓ BookAuthors has composite key (BookID, AuthorID)
✓ No attributes depend on only part of the key
Third Normal Form (3NF): No transitive dependencies
✓ MemberType stored directly in Members (not derived)
✓ Status in LoanWithDetails is computed, not stored
Data Structures:
- Records/Classes: Five entity classes with properties and methods
- Simple OOP: Encapsulation, constructors, validation methods
- Lists/Collections:
List<T>for managing collections
Algorithms:
- User-defined algorithms:
IsValid(),IsOverduelogic, email validation - File handling: SQLite database file operations
- Linear search: Finding records via SQL WHERE clauses
Database:
- Relational design: Normalised tables with relationships
- SQL queries: SELECT, INSERT, UPDATE, DELETE
- JOINs: INNER JOIN across multiple tables
Data Structures:
- Complex relational models: 5 interrelated tables
- Many-to-many relationships: BookAuthors junction table
- View models: LoanWithDetails for flattened JOIN data
Algorithms:
- Complex queries: 3-table JOINs
- Parameterised queries: SQL injection prevention
- Search algorithms: LIKE operator with wildcards
The Critical Security Rule
❌ NEVER do this (vulnerable to SQL injection):
string sql = "SELECT * FROM Books WHERE Title = '" + userInput + "'";If user enters: ' OR '1'='1 → returns all books!
If user enters: '; DROP TABLE Books; -- → deletes your table!
✅ ALWAYS do this (secure):
string sql = "SELECT * FROM Books WHERE Title = @Title";
cmd.Parameters.AddWithValue("@Title", userInput);How it works: Parameters are escaped and type-checked automatically. SQL engine treats them as pure data, never as executable code.
In this project: Every single SQL query uses parameters. Check DatabaseHelper.cs - you'll see @ParameterName throughout.
The Problem: A book can have multiple authors, and an author can write multiple books. You cannot represent this with foreign keys alone.
The Solution: Create a junction table (also called: link table, join table, associative entity).
How it works:
- Without Junction Table (WRONG):
Books table:
BookID | Title | AuthorIDs
1 | Good Omens | "8,9" ← Multiple values! Violates 1NF!
- With Junction Table (CORRECT):
Books:
BookID | Title
9 | Good Omens
Authors:
AuthorID | Name
8 | Terry Pratchett
9 | Neil Gaiman
BookAuthors:
BookID | AuthorID
9 | 8 ← Pratchett wrote Good Omens
9 | 9 ← Gaiman wrote Good Omens
In code:
// Link Good Omens to both authors
DatabaseHelper.AddBookAuthor(bookId: 9, authorId: 8); // Pratchett
DatabaseHelper.AddBookAuthor(bookId: 9, authorId: 9); // Gaiman
// Retrieve authors for a book
List<Author> authors = DatabaseHelper.GetAuthorsForBook(bookId: 9);
// Returns: [Terry Pratchett, Neil Gaiman]Purpose: Ensure data consistency across related tables.
Example:
CREATE TABLE Loans (
BookID INTEGER NOT NULL,
FOREIGN KEY (BookID) REFERENCES Books(BookID)
)This prevents:
- Creating a loan for BookID = 999 when no such book exists
- Orphaned records (loans pointing to deleted books)
CASCADE DELETE:
FOREIGN KEY (BookID) REFERENCES Books(BookID) ON DELETE CASCADEEffect: Deleting a book automatically deletes all its BookAuthors entries.
The Problem: Some fields can be empty/unknown. In Loans, ReturnDate is empty until the book is returned.
The Solution: Nullable value types using ?:
public DateTime? ReturnDate { get; set; } // Note the ?Working with nullable types:
// Check if it has a value
if (loan.ReturnDate.HasValue)
{
DateTime date = loan.ReturnDate.Value; // Extract the value
Console.WriteLine($"Returned on {date:dd/MM/yyyy}");
}
else
{
Console.WriteLine("Not yet returned");
}In database: NULL is used:
if (loan.ReturnDate.HasValue)
cmd.Parameters.AddWithValue("@ReturnDate", loan.ReturnDate.Value.ToString("yyyy-MM-dd"));
else
cmd.Parameters.AddWithValue("@ReturnDate", DBNull.Value);Purpose: Combine data from multiple related tables in one query.
Without JOIN (inefficient):
// 3 separate queries for each loan
List<Loan> loans = GetAllLoans();
foreach (Loan loan in loans)
{
Book book = GetBookById(loan.BookID); // 1 query per loan
Member member = GetMemberById(loan.MemberID); // 1 query per loan
}
// Total: 1 + (n × 2) queries for n loansWith JOIN (efficient):
SELECT
l.LoanID, l.LoanDate, l.DueDate,
b.Title AS BookTitle,
m.FirstName, m.LastName
FROM Loans l
INNER JOIN Books b ON l.BookID = b.BookID
INNER JOIN Members m ON l.MemberID = m.MemberIDTotal: 1 query regardless of number of loans!
INNER JOIN: Only returns rows where there's a match in both tables.
In this project: GetAllLoansWithDetails() uses 3-table JOIN to populate LoanWithDetails objects.
Create, Read, Update, Delete - the four fundamental database operations.
Every entity in this project has complete CRUD:
// CREATE
int newId = DatabaseHelper.InsertBook(book);
// READ
List<Book> all = DatabaseHelper.GetAllBooks();
Book one = DatabaseHelper.GetBookById(5);
// UPDATE
book.Title = "New Title";
DatabaseHelper.UpdateBook(book);
// DELETE
DatabaseHelper.DeleteBook(bookId);Pattern:
- Insert returns new ID (or -1 for failure)
- Update/Delete return bool (success/failure)
- Get methods return objects or collections
Three-Layer Validation:
Layer 1: UI (Immediate Feedback)
// In BtnSave_Click
if (string.IsNullOrWhiteSpace(txtTitle.Text))
{
MessageBox.Show("Title is required");
txtTitle.Focus(); // Take user to the problem
return;
}Layer 2: Model (Business Rules)
// In Book.cs
public bool IsValid()
{
if (string.IsNullOrWhiteSpace(Title)) return false;
if (YearPublished < 1000 || YearPublished > DateTime.Now.Year + 1) return false;
return true;
}Layer 3: Database (Final Enforcement)
CREATE TABLE Books (
Title TEXT NOT NULL, -- Database enforces NOT NULL
YearPublished INTEGER NOT NULL
)Why all three?
- UI: Fast feedback, good UX
- Model: Reusable logic, consistent rules
- Database: Absolute guarantee, protects against bugs
WPF is Microsoft's framework for building Windows desktop applications. It separates:
- XAML (
.xamlfiles): UI layout and design (XML-based) - C# (
.xaml.csfiles): Application logic and event handlers
Key Principle: Separation of Concerns - UI design is separate from behaviour.
XAML (Declarative UI):
<Button x:Name="btnSave"
Content="Save Book"
Click="BtnSave_Click"
Width="100"
Height="30"
Background="Green"/>Code-Behind (Event Logic):
private void BtnSave_Click(object sender, RoutedEventArgs e)
{
// What happens when button is clicked
SaveBook();
}Benefits:
- Designers can modify XAML without touching code
- Developers can change logic without breaking layout
- Clear separation between "what it looks like" and "what it does"
WPF applications don't run linearly from start to finish. Instead:
- Application starts → displays UI
- Waits for user action (idle)
- User clicks button → event fires
- Event handler executes
- Returns to waiting
Common Events in This Project:
// Button clicked
private void BtnSave_Click(object sender, RoutedEventArgs e) { }
// Selection changed in DataGrid
private void DgBooks_SelectionChanged(object sender, SelectionChangedEventArgs e) { }
// CheckBox checked/unchecked
private void ChkReturned_Checked(object sender, RoutedEventArgs e) { }
// Window closing
private void Window_Closing(object sender, CancelEventArgs e) { }Parameters:
sender: The control that raised the event (e.g., which button was clicked)e: Event arguments with additional information
Without Data Binding (manual, tedious):
foreach (Book book in books)
{
DataGridRow row = new DataGridRow();
// ... create cells manually
// ... set cell values
dgBooks.Rows.Add(row);
}
// Must repeat for every update!With Data Binding (automatic, elegant):
// Set once
dgBooks.ItemsSource = books;
// DataGrid automatically displays all book data!In XAML, specify which properties to display:
<DataGrid ItemsSource="{Binding}">
<DataGrid.Columns>
<DataGridTextColumn Header="Title" Binding="{Binding Title}"/>
<DataGridTextColumn Header="Year" Binding="{Binding YearPublished}"/>
</DataGrid.Columns>
</DataGrid>Key Concept: Binding connects UI element to object property. Changes sync automatically.
Pattern: Selecting item in list shows related details/data.
Implementation in BooksWindow:
private void DgBooks_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (dgBooks.SelectedItem != null)
{
Book book = (Book)dgBooks.SelectedItem;
// Populate form with book data
txtTitle.Text = book.Title;
txtISBN.Text = book.ISBN;
txtYear.Text = book.YearPublished.ToString();
// Show related authors
List<Author> authors = DatabaseHelper.GetAuthorsForBook(book.BookID);
lstAuthors.ItemsSource = authors;
}
}User Experience:
- User clicks book in DataGrid
- SelectionChanged event fires
- Form populates with book details
- Related authors list populates
- User can now edit or view relationships
This Project's Approach: Dedicated window for each major function.
Alternatives Considered:
| Approach | Pros | Cons | Why Not Chosen |
|---|---|---|---|
| Single Window + Tabs | All in one place | Complex XAML, harder navigation | Less modular |
| Single Window + UserControls | Flexible swapping | Complex state management | Over-engineered for exemplar |
| Multiple Windows ✓ | Focused, modular, flexible | More memory if many open | Best for teaching concepts |
Benefits of Multiple Windows:
- Each window has single, clear purpose
- Users can open multiple windows side-by-side (compare data)
- Development is modular - work on one window without affecting others
- Code organisation - related functionality grouped in one file
- Easy to add new windows without touching existing code
Creating a Window:
private void BtnManageBooks_Click(object sender, RoutedEventArgs e)
{
// 1. Create new instance
BooksWindow booksWindow = new BooksWindow();
// 2. Set owner (optional but recommended)
booksWindow.Owner = this;
// 3. Show the window
booksWindow.Show(); // Non-modal - both windows usable
}Modal vs Non-Modal:
Non-Modal (Show()):
window.Show(); // User can interact with both windows- Used in this project
- Good for: Reference windows, tools, multi-tasking
Modal (ShowDialog()):
bool? result = window.ShowDialog(); // Blocks until window closes
if (result == true)
{
// User clicked OK
}- User must close window before returning to parent
- Returns DialogResult
- Good for: Confirmations, required input, login screens
Window Owner Relationship:
childWindow.Owner = this;Effects:
- Child appears on top of parent
- Minimising parent minimises children
- Closing parent closes children
- Visual relationship clear to user
Containers arrange child controls. This project uses:
Most versatile - defines rows and columns:
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/> <!-- Size to content -->
<RowDefinition Height="*"/> <!-- Take remaining space -->
<RowDefinition Height="50"/> <!-- Fixed 50 pixels -->
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="2*"/> <!-- 2 parts of space -->
<ColumnDefinition Width="1*"/> <!-- 1 part of space -->
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Text="Header"/>
<DataGrid Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2"/>
</Grid>Sizing Options:
Auto: Size to content*: Proportional (takes remaining space)100: Fixed pixels2*: Takes twice as much space as1*
Stacks children vertically or horizontally:
<StackPanel Orientation="Vertical" Margin="10">
<TextBlock Text="Book Title:"/>
<TextBox x:Name="txtTitle"/>
<TextBlock Text="ISBN:"/>
<TextBox x:Name="txtISBN"/>
<Button Content="Save" Click="BtnSave_Click"/>
</StackPanel>Used for: Form fields, button groups, vertical/horizontal lists
Adds visual grouping:
<Border Background="White"
BorderBrush="Gray"
BorderThickness="1"
CornerRadius="5"
Padding="10">
<StackPanel>
<!-- Content -->
</StackPanel>
</Border>Used for: Sections, cards, visual separation
Equal-sized cells without row/column definitions:
<UniformGrid Rows="2" Columns="2">
<Button Content="Books"/>
<Button Content="Authors"/>
<Button Content="Members"/>
<Button Content="Loans"/>
</UniformGrid>Used for: Main menu buttons, uniform grids
Displays tabular data:
<DataGrid x:Name="dgBooks"
ItemsSource="{Binding}"
AutoGenerateColumns="False"
SelectionMode="Single"
IsReadOnly="True"
SelectionChanged="DgBooks_SelectionChanged">
<DataGrid.Columns>
<DataGridTextColumn Header="Title"
Binding="{Binding Title}"
Width="*"/>
<DataGridTextColumn Header="Year"
Binding="{Binding YearPublished}"
Width="100"/>
</DataGrid.Columns>
</DataGrid>Key Properties:
ItemsSource: Collection to display (set in code-behind)AutoGenerateColumns="False": We define columns manuallySelectionMode: Single, Multiple, ExtendedIsReadOnly="True": Prevents direct editing in gridWidth="*": Takes remaining space
For selecting from a list - crucial for foreign keys:
<ComboBox x:Name="cboMember"
DisplayMemberPath="FullName"
SelectedValuePath="MemberID"/>// Load data
cboMember.ItemsSource = DatabaseHelper.GetAllMembers();
// Get selected value
int selectedMemberId = (int)cboMember.SelectedValue; // Gets MemberIDKey Properties:
ItemsSource: Collection of objectsDisplayMemberPath: Which property to show (e.g., "FullName")SelectedValuePath: Which property to use as value (e.g., "MemberID")
Pattern for Foreign Keys:
// Show member name, but store member ID
cboMember.DisplayMemberPath = "FullName";
cboMember.SelectedValuePath = "MemberID";
cboMember.ItemsSource = members;
// When saving
loan.MemberID = (int)cboMember.SelectedValue;Displays list of items:
<ListBox x:Name="lstAuthors" ItemsSource="{Binding}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding FullName}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>ItemTemplate defines how each item displays.
Calendar control for dates:
<DatePicker x:Name="dpLoanDate"
SelectedDate="{Binding}"/>// Set date
dpLoanDate.SelectedDate = DateTime.Today;
// Get date
DateTime date = dpLoanDate.SelectedDate ?? DateTime.Today;Why DatePicker?
- Automatic date validation
- Calendar popup
- Prevents invalid dates (e.g., Feb 30)
- Better UX than TextBox
Text input:
<TextBox x:Name="txtTitle"
Width="200"/>// Get value
string title = txtTitle.Text;
// Set value
txtTitle.Text = "Default Title";
// Clear
txtTitle.Clear();
// Focus
txtTitle.Focus();Triggers actions:
<Button Content="Save Book"
Click="BtnSave_Click"
Width="100"
Height="30"
Background="Green"
Foreground="White"/>Styling Properties:
Background: Button colourForeground: Text colourFontWeight="Bold": Text weightFontSize="14": Text sizeCursor="Hand": Mouse cursor type
Same form for both operations:
// Class-level variable
private Book selectedBook = null;
// Add new book
private void BtnNew_Click(object sender, RoutedEventArgs e)
{
selectedBook = null; // Indicates "adding"
ClearForm();
}
// Edit selected book
private void DgBooks_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
selectedBook = (Book)dgBooks.SelectedItem;
if (selectedBook != null)
{
PopulateForm(selectedBook);
}
}
// Save button - works for both
private void BtnSave_Click(object sender, RoutedEventArgs e)
{
Book book = ValidateAndCreateBook();
if (selectedBook == null)
{
// Adding new
int newId = DatabaseHelper.InsertBook(book);
}
else
{
// Editing existing
book.BookID = selectedBook.BookID;
DatabaseHelper.UpdateBook(book);
}
}Benefits:
- Less code duplication
- Single validation logic
- Consistent user experience
- Easier to maintain
Principle: Bundle data and methods that operate on that data together.
In this project:
public class Book
{
// Data
public string Title { get; set; }
public int YearPublished { get; set; }
// Methods that operate on this data
public bool IsValid()
{
if (string.IsNullOrWhiteSpace(Title)) return false;
if (YearPublished < 1000) return false;
return true;
}
public string GetValidationErrors()
{
// Returns detailed error message
}
}Benefits:
- Book knows how to validate itself
- Validation logic not scattered through code
- Easy to test
- Changes to validation rules in one place
Principle: Different responsibilities in different places.
In this project:
Model Classes (Book, Author, Member, Loan)
├─ Represent data
├─ Validate data
└─ Business rules (IsOverdue, FullName)
DatabaseHelper
├─ All SQL queries
├─ Database connections
└─ Data conversion (SQL ↔ C# objects)
Window Classes (BooksWindow, etc.)
├─ Display data
├─ Capture user input
└─ Call DatabaseHelper methods
Example:
// ✗ BAD - mixing concerns
private void BtnSave_Click(...)
{
string sql = "INSERT INTO Books VALUES (@Title)"; // SQL in UI!
// ... database code
}
// ✓ GOOD - separated concerns
private void BtnSave_Click(...)
{
Book book = new Book { Title = txtTitle.Text };
DatabaseHelper.InsertBook(book); // UI just calls data layer
}Principle: Centralise all database operations.
Implementation: Static DatabaseHelper class with all CRUD methods.
Benefits:
- Single Location: All SQL in one file
- Reusability: Any window can call same methods
- Consistency: Same error handling everywhere
- Testability: Can test database operations independently
- Maintainability: Change SQL without touching UI code
Pattern:
// UI never writes SQL
List<Book> books = DatabaseHelper.GetAllBooks();
// DatabaseHelper handles SQL, connections, errors
public static List<Book> GetAllBooks()
{
try
{
using (SqliteConnection conn = new SqliteConnection(connectionString))
{
// SQL here
// Connection management here
// Error handling here
}
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message);
return new List<Book>(); // Return empty, don't crash
}
}Every database method wrapped in try-catch:
public static List<Book> GetAllBooks()
{
try
{
// Database operations
}
catch (Exception ex)
{
// Log error
System.Diagnostics.Debug.WriteLine($"Error: {ex.Message}");
// Return safe default
return new List<Book>();
}
}Why?
- Database operations can fail (locked files, connection issues, constraint violations)
- User shouldn't see technical errors
- Application shouldn't crash
ALWAYS use using for database connections:
using (SqliteConnection conn = new SqliteConnection(connectionString))
{
conn.Open();
// ... use connection
} // Automatically closed and disposed here, even if error occurs!What using does:
- Creates resource
- Executes code block
- Calls
Dispose()automatically (closes connection) - Works even if exception occurs
Without using (BAD):
SqliteConnection conn = new SqliteConnection(connectionString);
conn.Open();
// ... if error here, connection never closed!
conn.Close(); // Might not executeThree-stage validation:
// Stage 1: UI quick checks
if (string.IsNullOrWhiteSpace(txtTitle.Text))
{
MessageBox.Show("Title required");
return;
}
// Stage 2: Create object and validate
Book book = new Book { Title = txtTitle.Text };
if (!book.IsValid())
{
MessageBox.Show(book.GetValidationErrors());
return;
}
// Stage 3: Try database operation
try
{
int id = DatabaseHelper.InsertBook(book);
if (id > 0)
MessageBox.Show("Book saved successfully");
}
catch (Exception ex)
{
MessageBox.Show("Error saving book");
}string sql = @"SELECT * FROM Books WHERE Title LIKE @SearchTerm";
cmd.Parameters.AddWithValue("@SearchTerm", $"%{searchText}%");Wildcards:
%matches any characters (0 or more)searchTextbecomes%searchText%- Finds "searchText" anywhere in field
Examples:
%Potter%matches "Harry Potter", "Potter's Guide"Jane%matches "Jane Austen", "Janet Smith"%Smithmatches "John Smith", "Goldsmith"
When data already loaded:
List<LoanWithDetails> allLoans = DatabaseHelper.GetAllLoansWithDetails();
// Filter in memory using LINQ
var overdueLoans = allLoans.Where(loan => loan.IsOverdue).ToList();
var activeLoans = allLoans.Where(loan => !loan.IsReturned).ToList();
var studentLoans = allLoans.Where(loan => loan.MemberType == "Student").ToList();When to use:
- Small datasets (< 10,000 records)
- Multiple filters on same data
- Avoids repeated database queries
- Faster for subsequent filters
Query only needed data:
SELECT * FROM Loans
WHERE ReturnDate IS NULL -- Not returned
AND DueDate < date('now') -- Past due dateWhen to use:
- Large datasets
- Single filter operation
- Reduces memory usage
- Only selected data transferred
- Meaningful names:
GetAuthorsForBook,IsOverdue,connectionString - Comprehensive comments: Every method has XML summary explaining purpose, parameters, returns
- Consistent style: PascalCase for methods, camelCase for parameters, indentation consistent
- Modular code: Separate classes for models, database, UI
- Appropriate variables: Local variables used, minimal class-level state
- Constants:
connectionStringas field - Self-documenting: Names clearly indicate purpose
- Consistent formatting: Same patterns throughout
- Cohesive methods: Each does one thing well (
InsertBook,UpdateBook, notInsertOrUpdateBook) - Well-designed parameters: Methods take what they need, return meaningful values
- Defensive programming: Validation at multiple levels, null checks
- Error handling: Try-catch throughout database operations
- Clear separation: Data, logic, UI in separate layers
- Visual Studio 2019+ (Community Edition is free)
- Download: https://visualstudio.microsoft.com/
- .NET Desktop Development workload installed
- NuGet Package Manager (included with Visual Studio)
- Open Solution
File → Open → Project/Solution
Select DatabaseExampleWPF.sln
- Install SQLite Package (CRITICAL!)
Right-click project → Manage NuGet Packages
Browse → Search "Microsoft.Data.Sqlite"
Install "Microsoft.Data.Sqlite" by Microsoft
OR via Package Manager Console:
Tools → NuGet Package Manager → Package Manager Console
Type: Install-Package Microsoft.Data.Sqlite
Press Enter
- Build Project
Build → Build Solution (Ctrl+Shift+B)
Check Output window for errors
- Run Application
Press F5 or click Start button
Main window appears
- Initialize Database
Click "Create Database & Tables"
Click "Add Sample Data" (recommended)
- Explore Features
- Open each management window
- Add, edit, delete records
- Create book-author relationships
- Search loans with filters
- Download: https://sqlitebrowser.org/
- Open database:
LibraryDatabase.dbfrombin/Debugfolder - View tables: Browse Data tab
- Execute SQL: Execute SQL tab
- Understand structure: Database Structure tab
Example queries to try:
-- All books with authors
SELECT b.Title, a.FirstName, a.LastName
FROM Books b
INNER JOIN BookAuthors ba ON b.BookID = ba.BookID
INNER JOIN Authors a ON ba.AuthorID = a.AuthorID;
-- Overdue loans
SELECT b.Title, m.FirstName, m.LastName, l.DueDate
FROM Loans l
INNER JOIN Books b ON l.BookID = b.BookID
INNER JOIN Members m ON l.MemberID = m.MemberID
WHERE l.ReturnDate IS NULL AND l.DueDate < date('now');- Variables, constants, data types
- String handling (
string.IsNullOrWhiteSpace,$"{FirstName} {LastName}") - Boolean operations (
IsOverdue,IsReturned) - Exception handling (try-catch throughout)
- OOP concepts (classes, properties, methods, encapsulation)
- Records/classes with properties
- Lists and collections (
List<Book>,List<Author>) - Nullable types (
DateTime?)
- Searching (SQL WHERE, LIKE operator)
- Validation algorithms (
IsValid,IsValidEmailwith regex) - Date calculations (
DaysUntilDue)
- Character encoding (TEXT fields in SQLite)
- Date representation (ISO 8601 format: YYYY-MM-DD)
- Email format validation (regex pattern matching)
- Relational databases: 5 related tables
- Normalisation: 1NF, 2NF, 3NF demonstrated
- SQL: SELECT, INSERT, UPDATE, DELETE
- JOIN operations: INNER JOIN across 3 tables
- Primary keys: AUTOINCREMENT
- Foreign keys: Referential integrity with CASCADE
- Entity relationships: One-to-many, many-to-many
- Analysis: Problem decomposition into entities
- Design: ERD, database schema, class structure
- Implementation: Complete working solution
- Testing: Sample data with varied scenarios
- Evaluation: Comments explaining decisions
Table 1 - Technical Skills:
- Group B: Records, simple OOP, file handling (database), linear search
- Group A: Complex data structures (many-to-many), advanced queries (JOINs), parameterised queries
Table 2 - Coding Style:
- Basic: Meaningful names, extensive comments, consistent style
- Good: Well-designed UI, strong modularisation, appropriate variables
- Excellent: Cohesive methods, defensive programming, clear separation of concerns
// NEVER
string sql = $"SELECT * FROM Users WHERE Name = '{name}'";
// ALWAYS
string sql = "SELECT * FROM Users WHERE Name = @Name";
cmd.Parameters.AddWithValue("@Name", name);- UI: Immediate feedback
- Model: Business rules
- Database: Final enforcement
- Models: Data and validation
- DatabaseHelper: SQL and database operations
- Windows: UI and user interaction
using (SqliteConnection conn = new SqliteConnection(connectionString))
{
// Connection automatically closed
}try
{
DatabaseHelper.DeleteBook(id);
}
catch (Exception ex)
{
MessageBox.Show("Cannot delete book with active loans");
}In NEA documentation, explain:
- Why this database structure?
- Why these data types?
- How does design prevent invalid data?
- What alternatives were considered?
- How to design normalised databases
- How to use foreign keys and relationships
- How to write parameterised queries
- How to implement CRUD operations
- How to structure database applications
- How to manage multiple windows in WPF
- How to separate concerns (UI, logic, data)
- Don't copy this code into your NEA
- Don't use this as a template
- Don't create a library system unless it's YOUR original idea
- Don't submit code you don't fully understand
- A problem YOU identified through investigation
- Designed BY YOU based on requirements YOU gathered
- Implemented using concepts in YOUR OWN WAY
- Demonstrating YOUR problem-solving skills
- YOUR original work with YOUR analysis and evaluation
Remember: Examiners look for evidence of YOUR thinking, YOUR decisions, YOUR problem-solving.
- AQA 7517 Specification: Section 4.14
- Microsoft C# Docs: https://docs.microsoft.com/en-us/dotnet/csharp/
- SQLite Docs: https://www.sqlite.org/docs.html
- WPF Tutorial: https://docs.microsoft.com/en-us/dotnet/desktop/wpf/
- Normalisation principles (1NF, 2NF, 3NF)
- Entity Relationship Diagrams (ERD)
- Primary and foreign key design
- OWASP SQL Injection Prevention
- Parameterised queries best practices
- SQL Injection: Never concatenate strings for SQL
- Connection Leaks: Always use
usingstatements - Poor Error Handling: Wrap database operations in try-catch
- Denormalised Data: Follow normalisation principles
- Missing Validation: Validate at UI, model, and database levels
- Tight Coupling: Keep SQL separate from UI code
- Add "Publisher" field to Books table
- Add ISBN validation (13 digits)
- Add phone number to Members
- Create Reservations feature
- Add Genre table (many-to-many with Books)
- Implement late fees calculation
- Add user authentication
- Create audit log (track changes)
- Multi-criteria search (AND/OR logic)
- Export to CSV
- Barcode scanning
- Statistics dashboard
- Email notifications for overdue books
- Recommendation system
Created by: Claude AI (Anthropic)
Purpose: Educational exemplar for AQA 7517 A-Level Computer Science NEA
Date: November 2024
Framework: WPF with .NET Framework 4.7.2+
Database: SQLite with Microsoft.Data.Sqlite
Licence: Provided for educational purposes
- AQA for A-Level Computer Science specification
- Microsoft for WPF, C#, and SQLite libraries
- SQLite team for the database engine
- DB Browser for SQLite team
This exemplar demonstrates professional database application development. The concepts are fundamental to any database-driven project.
However, your NEA must be:
- Your own idea - addressing a real problem you've identified
- Your own design - based on your analysis and planning
- Your own code - implementing concepts in your own way
- Your own work - demonstrating your problem-solving ability
Use this to learn concepts, not to copy solutions.
Apply these principles to your own project, and create something you can be proud of - that demonstrates your true capabilities.
For Teachers: Use this to demonstrate concepts in lessons, but students must not use it as an NEA starting point.
For Students: Learn from this, understand the concepts, then create your own original work. Your NEA should reflect YOUR problem-solving skills.
