The MVC Project Manager proves that ASP.NET Core MVC doesn't have to feel dated. Each Razor view pairs with a small .cshtml.js file that mounts a Vue 3 application, turning server-rendered pages into reactive UIs that talk to VSA API endpoints. This part shows how to build that frontend: mounting Vue apps, integrating DataTables.js for server-side grids, handling drag-and-drop file uploads, building an image gallery, and supporting print-to-PDF.
The architecture is simple: the Razor view provides the HTML skeleton and initial data, the .cshtml.js file enhances it with Vue reactivity, and the VSA endpoints handle all data operations. Pages work even without JavaScript (progressive enhancement), but become fully interactive when Vue is present. This is a key advantage of the MVC approach to VSA frontends.
Mounting a Vue 3 App per Page
Each page mounts its own Vue app with a mount point in the Razor view. The create form's Vue app manages the entire form state — fields, validation, items, attachments, and submission:
// Create.cshtml.js
const app = createApp({
data() {
return {
form: { name: '', description: '', priority: 'Medium',
category: 'Personal', progress: 0, tags: '',
dueDate: null, dueTime: null, ownerUserId: null },
items: [], imageAttachments: [], fileAttachments: [],
submitting: false
}
},
methods: {
async submitForm() {
this.submitting = true;
const response = await fetch('/api/todo', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(this.form)
});
if (response.ok) { window.location.href = '/Main/Todo/Index'; }
}
}
});
app.mount('#app-create');
The form object is the Vue data model. It maps exactly to the CreateTodoRequest DTO on the backend — the JSON body sent to POST /api/todo deserializes directly into the request. This symmetry between the Vue data model and the VSA request DTO is what makes the frontend predictable.
Server-Side DataTables Integration
The index page uses DataTables.js with server-side processing. The table sends pagination, search, and sort parameters to the VSA endpoint and renders the JSON response:
$('#todoTable').DataTable({
serverSide: true,
processing: true,
ajax: { url: '/api/todo', dataSrc: 'data.data' },
columns: [
{ data: 'autoNumber', title: 'No' },
{ data: 'name', title: 'Name' },
{ data: 'priority', title: 'Priority', render: priorityBadge },
{ data: 'progress', title: 'Progress', render: progressBar },
{ data: 'status', title: 'Status', render: statusBadge },
{ data: 'itemsCount', title: 'Items' }
],
order: [[1, 'asc']]
});
Custom render functions transform raw values into visual components: priority becomes a color-coded badge, progress becomes a bootstrap progress bar, and status becomes a completed/pending chip. The DataTable calls the GetTodoListHandler we built in Part 8 — search, sort, and pagination all happen server-side in the VSA handler.
Drag-and-Drop File Upload with Base64
The MVC frontend handles file uploads entirely in the browser. When the user drops a file, Vue.js reads it as a base64 string, validates the type and size, and adds it to the reactive attachments array:
async handleFileDrop(event, type) {
const files = event.dataTransfer.files;
for (const file of files) {
if (type === 'image' && !file.name.endsWith('.png')) {
this.uploadError = 'Only PNG images are allowed';
return;
}
if (file.size > 5 * 1024 * 1024) {
this.uploadError = 'File must be under 5MB';
return;
}
const base64 = await this.readFileAsBase64(file);
this.imageAttachments.push({
id: null, fileName: file.name, data: base64
});
}
}
The base64 string is included in the JSON payload, matching the CreateTodoImageAttachmentRequest DTO with its Data field (Part 7). The backend's FileStorageService decodes and saves the file. A thumbnail preview renders immediately using a data URL, giving instant visual feedback.
Image Gallery with Prev/Next Navigation
The detail page's image gallery uses a Bootstrap modal with prev/next navigation. Clicking a thumbnail opens the modal; the arrows cycle through the attachments. The gallery is powered by Vue's reactive index state:
<div class="modal fade" id="imageGallery">
<img :src="galleryImages[galleryIndex].dataUrl" />
<button @click="galleryIndex = (galleryIndex - 1 + galleryImages.length)
% galleryImages.length">Prev</button>
<button @click="galleryIndex = (galleryIndex + 1) % galleryImages.length">Next</button>
</div>
The modulo arithmetic wraps the index around, creating infinite prev/next navigation. This is a small but polished UX detail that makes the MVC frontend feel as refined as a dedicated SPA.
Print-to-PDF Support
The detail page supports print-to-PDF via the browser's native print functionality, enhanced with a clean print stylesheet. The window.print() call produces a PDF-friendly layout — the audit trail card, attachments, and items table all print cleanly. This is a zero-dependency approach that works everywhere: no print library, no server-side PDF generation, no license costs.
Form Enhancements: Tom Select and Flatpickr
The create and edit forms use Tom Select for searchable dropdowns (priority, owner, assigned-to) and flatpickr for date/time pickers. The Edit page's Vue app loads existing data from the VSA endpoints, re-initializes Tom Select and flatpickr after data load (with proper destroy/recreate to avoid stale state), and submits via PUT /api/todo. The form handles the full lifecycle: load → edit → submit → reload.