Apply when working with classic fullstack patterns including jQuery AJAX, form handling, and C# MVC integration
Implements classic fullstack patterns with C# MVC, jQuery AJAX, and Razor forms.
/plugin marketplace add twofoldtech-dakota/claude-marketplace/plugin install sitecore-classic-analyzer@cms-analyzers-marketplaceThis skill inherits all available tools. When active, it can use any tool Claude has access to.
// Controller
public class ContactController : Controller
{
private readonly IContactService _contactService;
private readonly ILogger<ContactController> _logger;
public ContactController(
IContactService contactService,
ILogger<ContactController> logger)
{
_contactService = contactService;
_logger = logger;
}
[HttpGet]
public IActionResult Index()
{
return View(new ContactFormModel());
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Index(ContactFormModel model, CancellationToken ct)
{
if (!ModelState.IsValid)
{
return View(model);
}
try
{
await _contactService.ProcessContactAsync(model, ct);
TempData["SuccessMessage"] = "Thank you for your message!";
return RedirectToAction(nameof(Index));
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to process contact form");
ModelState.AddModelError("", "An error occurred. Please try again.");
return View(model);
}
}
}
@model ContactFormModel
@if (TempData["SuccessMessage"] != null)
{
<div class="alert alert-success">
@TempData["SuccessMessage"]
</div>
}
<form asp-action="Index" method="post">
@Html.AntiForgeryToken()
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<label asp-for="Name"></label>
<input asp-for="Name" class="form-control" />
<span asp-validation-for="Name" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Email"></label>
<input asp-for="Email" class="form-control" type="email" />
<span asp-validation-for="Email" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Message"></label>
<textarea asp-for="Message" class="form-control" rows="5"></textarea>
<span asp-validation-for="Message" class="text-danger"></span>
</div>
<button type="submit" class="btn btn-primary">Send Message</button>
</form>
@section Scripts {
<partial name="_ValidationScriptsPartial" />
}
// JavaScript
$(document).ready(function() {
$('#contact-form').on('submit', function(e) {
e.preventDefault();
var $form = $(this);
var $submitBtn = $form.find('button[type="submit"]');
var $result = $('#form-result');
// Disable button and show loading state
$submitBtn.prop('disabled', true).text('Sending...');
$result.empty();
$.ajax({
url: $form.attr('action'),
type: 'POST',
data: $form.serialize(),
success: function(response) {
if (response.success) {
$result.html('<div class="alert alert-success">' + response.message + '</div>');
$form[0].reset();
} else {
showValidationErrors(response.errors);
}
},
error: function(xhr, status, error) {
$result.html('<div class="alert alert-danger">An error occurred. Please try again.</div>');
console.error('Form submission failed:', error);
},
complete: function() {
$submitBtn.prop('disabled', false).text('Send Message');
}
});
});
function showValidationErrors(errors) {
// Clear previous errors
$('.field-validation-error').text('');
$('.input-validation-error').removeClass('input-validation-error');
// Show new errors
$.each(errors, function(field, messages) {
var $field = $('[name="' + field + '"]');
$field.addClass('input-validation-error');
$field.siblings('.field-validation-error').text(messages.join(', '));
});
}
});
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> SubmitAjax(ContactFormModel model, CancellationToken ct)
{
if (!ModelState.IsValid)
{
var errors = ModelState
.Where(x => x.Value.Errors.Count > 0)
.ToDictionary(
kvp => kvp.Key,
kvp => kvp.Value.Errors.Select(e => e.ErrorMessage).ToArray()
);
return Json(new { success = false, errors });
}
try
{
await _contactService.ProcessContactAsync(model, ct);
return Json(new { success = true, message = "Thank you for your message!" });
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to process contact form");
return Json(new {
success = false,
errors = new { General = new[] { "An error occurred. Please try again." } }
});
}
}
// Setup for all AJAX requests
$.ajaxSetup({
beforeSend: function(xhr, settings) {
if (settings.type === 'POST' || settings.type === 'PUT' || settings.type === 'DELETE') {
var token = $('input[name="__RequestVerificationToken"]').val();
if (token) {
xhr.setRequestHeader('RequestVerificationToken', token);
}
}
}
});
// Or include in data for form-encoded requests
$.ajax({
url: '/api/items',
type: 'POST',
data: {
__RequestVerificationToken: $('input[name="__RequestVerificationToken"]').val(),
name: 'New Item'
}
});
@* Add to _Layout.cshtml for global availability *@
<form id="__AjaxAntiForgeryForm" action="#" method="post">
@Html.AntiForgeryToken()
</form>
// Get token from hidden form
function getAntiForgeryToken() {
return $('#__AjaxAntiForgeryForm input[name="__RequestVerificationToken"]').val();
}
// Controller
[HttpGet]
public async Task<IActionResult> LoadProducts(
int page = 1,
string category = null,
CancellationToken ct = default)
{
var products = await _productService.GetPagedAsync(page, 12, category, ct);
return PartialView("_ProductGrid", products);
}
[HttpGet]
public async Task<IActionResult> ProductDetails(Guid id, CancellationToken ct)
{
var product = await _productService.GetByIdAsync(id, ct);
if (product == null)
{
return NotFound();
}
return PartialView("_ProductDetails", product);
}
// Load more products
$('#load-more').on('click', function() {
var $btn = $(this);
var page = parseInt($btn.data('page')) + 1;
var category = $btn.data('category');
$btn.prop('disabled', true).text('Loading...');
$.get('/Products/LoadProducts', { page: page, category: category })
.done(function(html) {
$('#product-grid').append(html);
$btn.data('page', page);
})
.fail(function() {
alert('Failed to load products');
})
.always(function() {
$btn.prop('disabled', false).text('Load More');
});
});
// Load product details in modal
$(document).on('click', '[data-product-details]', function(e) {
e.preventDefault();
var productId = $(this).data('product-details');
$.get('/Products/ProductDetails/' + productId)
.done(function(html) {
$('#modal-content').html(html);
$('#product-modal').modal('show');
})
.fail(function() {
alert('Failed to load product details');
});
});
var MYAPP = MYAPP || {};
MYAPP.search = (function($) {
var debounceTimer;
var $input;
var $results;
var minChars = 3;
var debounceDelay = 300;
function init() {
$input = $('#search-input');
$results = $('#search-results');
$input.on('keyup', function() {
var query = $(this).val().trim();
clearTimeout(debounceTimer);
if (query.length < minChars) {
$results.empty().hide();
return;
}
debounceTimer = setTimeout(function() {
performSearch(query);
}, debounceDelay);
});
// Close results when clicking outside
$(document).on('click', function(e) {
if (!$(e.target).closest('.search-container').length) {
$results.hide();
}
});
}
function performSearch(query) {
$results.html('<div class="search-loading">Searching...</div>').show();
$.get('/Search/Results', { q: query })
.done(function(html) {
$results.html(html).show();
})
.fail(function() {
$results.html('<div class="search-error">Search failed</div>');
});
}
return { init: init };
})(jQuery);
$(document).ready(function() {
MYAPP.search.init();
});
[HttpGet]
public async Task<IActionResult> Results(string q, CancellationToken ct)
{
if (string.IsNullOrWhiteSpace(q) || q.Length < 3)
{
return PartialView("_NoResults");
}
var results = await _searchService.SearchAsync(q, maxResults: 10, ct);
return PartialView("_SearchResults", results);
}
[HttpGet]
public async Task<IActionResult> Index(int page = 1, CancellationToken ct = default)
{
const int pageSize = 12;
var result = await _productService.GetPagedAsync(page, pageSize, ct);
ViewBag.CurrentPage = page;
ViewBag.TotalPages = result.TotalPages;
ViewBag.HasPrevious = page > 1;
ViewBag.HasNext = page < result.TotalPages;
return View(result.Items);
}
@* _Pagination.cshtml *@
@{
var currentPage = (int)ViewBag.CurrentPage;
var totalPages = (int)ViewBag.TotalPages;
var hasPrevious = (bool)ViewBag.HasPrevious;
var hasNext = (bool)ViewBag.HasNext;
}
@if (totalPages > 1)
{
<nav aria-label="Page navigation">
<ul class="pagination">
<li class="page-item @(!hasPrevious ? "disabled" : "")">
<a class="page-link"
asp-action="Index"
asp-route-page="@(currentPage - 1)"
aria-label="Previous">
<span aria-hidden="true">«</span>
</a>
</li>
@for (int i = 1; i <= totalPages; i++)
{
<li class="page-item @(i == currentPage ? "active" : "")">
<a class="page-link" asp-action="Index" asp-route-page="@i">@i</a>
</li>
}
<li class="page-item @(!hasNext ? "disabled" : "")">
<a class="page-link"
asp-action="Index"
asp-route-page="@(currentPage + 1)"
aria-label="Next">
<span aria-hidden="true">»</span>
</a>
</li>
</ul>
</nav>
}
$(document).on('click', '.pagination a', function(e) {
e.preventDefault();
var url = $(this).attr('href');
$.get(url)
.done(function(html) {
$('#content-container').html(html);
// Update browser URL without reload
history.pushState(null, '', url);
})
.fail(function() {
alert('Failed to load page');
});
});
// Handle browser back/forward
$(window).on('popstate', function() {
$.get(location.href)
.done(function(html) {
$('#content-container').html(html);
});
});
<form asp-action="Upload" method="post" enctype="multipart/form-data">
@Html.AntiForgeryToken()
<div class="form-group">
<label for="file">Select file</label>
<input type="file" name="file" id="file" class="form-control-file" accept=".jpg,.png,.pdf" />
<small class="form-text text-muted">Max size: 5MB. Allowed: JPG, PNG, PDF</small>
</div>
<button type="submit" class="btn btn-primary">Upload</button>
</form>
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Upload(IFormFile file, CancellationToken ct)
{
if (file == null || file.Length == 0)
{
ModelState.AddModelError("file", "Please select a file");
return View();
}
if (file.Length > 5 * 1024 * 1024) // 5MB
{
ModelState.AddModelError("file", "File size cannot exceed 5MB");
return View();
}
var allowedExtensions = new[] { ".jpg", ".png", ".pdf" };
var extension = Path.GetExtension(file.FileName).ToLowerInvariant();
if (!allowedExtensions.Contains(extension))
{
ModelState.AddModelError("file", "Invalid file type");
return View();
}
var fileName = $"{Guid.NewGuid()}{extension}";
var filePath = Path.Combine(_uploadPath, fileName);
using (var stream = new FileStream(filePath, FileMode.Create))
{
await file.CopyToAsync(stream, ct);
}
TempData["SuccessMessage"] = "File uploaded successfully";
return RedirectToAction(nameof(Index));
}
$('#upload-form').on('submit', function(e) {
e.preventDefault();
var formData = new FormData(this);
var $progress = $('#upload-progress');
var $progressBar = $progress.find('.progress-bar');
$progress.show();
$.ajax({
url: $(this).attr('action'),
type: 'POST',
data: formData,
processData: false,
contentType: false,
xhr: function() {
var xhr = new window.XMLHttpRequest();
xhr.upload.addEventListener('progress', function(e) {
if (e.lengthComputable) {
var percent = Math.round((e.loaded / e.total) * 100);
$progressBar.css('width', percent + '%').text(percent + '%');
}
});
return xhr;
},
success: function(response) {
if (response.success) {
showSuccess('File uploaded successfully');
$('#file-input').val('');
} else {
showError(response.message);
}
},
error: function() {
showError('Upload failed');
},
complete: function() {
setTimeout(function() {
$progress.hide();
$progressBar.css('width', '0%').text('');
}, 1000);
}
});
});
$(document).ajaxError(function(event, xhr, settings, error) {
if (xhr.status === 401) {
// Redirect to login
window.location.href = '/Account/Login?returnUrl=' + encodeURIComponent(window.location.pathname);
return;
}
if (xhr.status === 403) {
showError('You do not have permission to perform this action');
return;
}
if (xhr.status === 404) {
showError('The requested resource was not found');
return;
}
if (xhr.status >= 500) {
showError('A server error occurred. Please try again later.');
return;
}
console.error('AJAX error:', settings.url, error);
});
// Controller returning JSON errors
[HttpPost]
public IActionResult Process(ProcessRequest request)
{
try
{
// Process...
return Json(new { success = true });
}
catch (ValidationException ex)
{
return BadRequest(new {
success = false,
errors = ex.Errors
});
}
catch (Exception ex)
{
_logger.LogError(ex, "Processing failed");
return StatusCode(500, new {
success = false,
message = "An unexpected error occurred"
});
}
}
$.ajax({
url: '/api/process',
type: 'POST',
data: formData,
success: function(response) {
if (response.success) {
showSuccess('Operation completed');
}
},
error: function(xhr) {
if (xhr.responseJSON) {
if (xhr.responseJSON.errors) {
displayFieldErrors(xhr.responseJSON.errors);
} else if (xhr.responseJSON.message) {
showError(xhr.responseJSON.message);
}
} else {
showError('An error occurred');
}
}
});
// Store in session
HttpContext.Session.SetString("UserPreference", "dark");
HttpContext.Session.SetInt32("CartCount", 5);
// Complex objects
HttpContext.Session.SetString("Cart", JsonSerializer.Serialize(cart));
// Retrieve from session
var preference = HttpContext.Session.GetString("UserPreference");
var cartCount = HttpContext.Session.GetInt32("CartCount");
var cart = JsonSerializer.Deserialize<Cart>(HttpContext.Session.GetString("Cart"));
// Set cookie
function setCookie(name, value, days) {
var expires = '';
if (days) {
var date = new Date();
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
expires = '; expires=' + date.toUTCString();
}
document.cookie = name + '=' + encodeURIComponent(value) + expires + '; path=/';
}
// Get cookie
function getCookie(name) {
var nameEQ = name + '=';
var cookies = document.cookie.split(';');
for (var i = 0; i < cookies.length; i++) {
var cookie = cookies[i].trim();
if (cookie.indexOf(nameEQ) === 0) {
return decodeURIComponent(cookie.substring(nameEQ.length));
}
}
return null;
}
// Delete cookie
function deleteCookie(name) {
setCookie(name, '', -1);
}
var MYAPP = MYAPP || {};
MYAPP.toast = (function($) {
var $container;
function init() {
$container = $('<div id="toast-container"></div>').appendTo('body');
}
function show(message, type, duration) {
type = type || 'info';
duration = duration || 5000;
var $toast = $('<div class="toast toast-' + type + '">' +
'<span class="toast-message">' + message + '</span>' +
'<button class="toast-close">×</button>' +
'</div>');
$container.append($toast);
setTimeout(function() {
$toast.addClass('show');
}, 10);
var timer = setTimeout(function() {
remove($toast);
}, duration);
$toast.find('.toast-close').on('click', function() {
clearTimeout(timer);
remove($toast);
});
}
function remove($toast) {
$toast.removeClass('show');
setTimeout(function() {
$toast.remove();
}, 300);
}
function success(message) { show(message, 'success'); }
function error(message) { show(message, 'error'); }
function warning(message) { show(message, 'warning'); }
function info(message) { show(message, 'info'); }
return {
init: init,
show: show,
success: success,
error: error,
warning: warning,
info: info
};
})(jQuery);
$(document).ready(function() {
MYAPP.toast.init();
});
#toast-container {
position: fixed;
top: 20px;
right: 20px;
z-index: 9999;
}
.toast {
min-width: 300px;
padding: 15px 40px 15px 15px;
margin-bottom: 10px;
border-radius: 4px;
opacity: 0;
transform: translateX(100%);
transition: all 0.3s ease;
&.show {
opacity: 1;
transform: translateX(0);
}
&-success { background: #28a745; color: white; }
&-error { background: #dc3545; color: white; }
&-warning { background: #ffc107; color: #333; }
&-info { background: #17a2b8; color: white; }
&-close {
position: absolute;
top: 10px;
right: 10px;
background: none;
border: none;
color: inherit;
font-size: 20px;
cursor: pointer;
opacity: 0.7;
&:hover { opacity: 1; }
}
}