Events-MVC (example with htmx)
This commit is contained in:
28
Events-MVC/Events.MVC/Util/Extensions/ExceptionExtensions.cs
Normal file
28
Events-MVC/Events.MVC/Util/Extensions/ExceptionExtensions.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
using System.Text;
|
||||
|
||||
namespace Events.MVC.Util.Extensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Class with useful extensions for exceptions handling
|
||||
/// </summary>
|
||||
public static class ExceptionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// return complete hierarchy of an exception. It checks whether the exception has inner exception,
|
||||
/// and if it has, then it appends inner exception message.
|
||||
/// Then it looks for inner exception of the inner exceptions, and so on.
|
||||
/// </summary>
|
||||
/// <param name="exc">Exception which message hiearchy should be obtained</param>
|
||||
/// <returns>String containing all exception hierarchy messages</returns>
|
||||
public static string CompleteExceptionMessage(this Exception? exc)
|
||||
{
|
||||
StringBuilder sb = new();
|
||||
while (exc != null)
|
||||
{
|
||||
sb.AppendLine(exc.Message);
|
||||
exc = exc.InnerException;
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using System.Text;
|
||||
|
||||
namespace Events.MVC.Util.Extensions
|
||||
{
|
||||
public static class ModelStateExtensions
|
||||
{
|
||||
public static string GetErrorsString(this ModelStateDictionary modelState)
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
foreach (var modelStateEntry in modelState)
|
||||
{
|
||||
if (modelStateEntry.Value.Errors.Count > 0)
|
||||
{
|
||||
string key = modelStateEntry.Key;
|
||||
string error = string.Join(", ", modelStateEntry.Value.Errors.Select(e => e.ErrorMessage));
|
||||
sb.AppendFormat("{0}: {1}; ", key, error);
|
||||
}
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
using Sieve.Models;
|
||||
|
||||
namespace Events.MVC.Util.Extensions;
|
||||
|
||||
public static class SieveModelExtensions
|
||||
{
|
||||
public static void SetDefaultPagingAndSorting(this SieveModel sieveModel, int defaultPageSize, string defaultSort)
|
||||
{
|
||||
sieveModel.Page ??= 1;
|
||||
|
||||
if (sieveModel.Page < 1)
|
||||
{
|
||||
sieveModel.Page = 1;
|
||||
}
|
||||
|
||||
if (sieveModel.PageSize is null || sieveModel.PageSize <= 0)
|
||||
{
|
||||
sieveModel.PageSize = defaultPageSize;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(sieveModel.Sorts))
|
||||
{
|
||||
sieveModel.Sorts = defaultSort;
|
||||
}
|
||||
}
|
||||
|
||||
public static string ExtractFilterValue(this SieveModel sieveModel, string propertyName)
|
||||
{
|
||||
var filters = sieveModel.Filters?.Trim() ?? string.Empty;
|
||||
return ExtractFilterValue(filters, propertyName);
|
||||
}
|
||||
|
||||
public static string ExtractFilterValue(string filters, string propertyName)
|
||||
{
|
||||
return ExtractFilterValue(filters, propertyName, "@=*", "@=");
|
||||
}
|
||||
|
||||
public static string ExtractFilterValue(string filters, string propertyName, params string[] operators)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(filters))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
foreach (var filter in filters.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
|
||||
{
|
||||
foreach (var filterOperator in operators)
|
||||
{
|
||||
var prefix = $"{propertyName}{filterOperator}";
|
||||
if (filter.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return filter[prefix.Length..];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
10
Events-MVC/Events.MVC/Util/Extensions/StringExtensions.cs
Normal file
10
Events-MVC/Events.MVC/Util/Extensions/StringExtensions.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace Events.MVC.Util.Extensions;
|
||||
|
||||
public static class StringExtensions
|
||||
{
|
||||
public static string? TrimToNull(this string? value)
|
||||
{
|
||||
var trimmed = value?.Trim();
|
||||
return string.IsNullOrEmpty(trimmed) ? null : trimmed;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
using Events.MVC.Util.Extensions;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Npgsql;
|
||||
|
||||
namespace Events.MVC.Util.Middleware;
|
||||
|
||||
public class ProblemDetailsForSqlException : ExceptionFilterAttribute
|
||||
{
|
||||
private readonly ILogger<ProblemDetailsForSqlException> logger;
|
||||
|
||||
public ProblemDetailsForSqlException(ILogger<ProblemDetailsForSqlException> logger)
|
||||
{
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
public override void OnException(ExceptionContext context)
|
||||
{
|
||||
Exception? exception = context.Exception;
|
||||
PostgresException? postgresException = null;
|
||||
|
||||
while (exception is not null)
|
||||
{
|
||||
if (exception is PostgresException currentPostgresException)
|
||||
{
|
||||
postgresException = currentPostgresException;
|
||||
break;
|
||||
}
|
||||
|
||||
if (exception is DbUpdateException dbUpdateException && dbUpdateException.InnerException is not null)
|
||||
{
|
||||
exception = dbUpdateException.InnerException;
|
||||
continue;
|
||||
}
|
||||
|
||||
exception = exception.InnerException;
|
||||
}
|
||||
|
||||
if (postgresException is null)
|
||||
{
|
||||
base.OnException(context);
|
||||
return;
|
||||
}
|
||||
|
||||
ProblemDetails problemDetails = postgresException.SqlState switch
|
||||
{
|
||||
PostgresErrorCodes.UniqueViolation => new ProblemDetails
|
||||
{
|
||||
Title = "Duplicate data",
|
||||
Detail = "A record with the same data already exists."
|
||||
},
|
||||
PostgresErrorCodes.ForeignKeyViolation => new ProblemDetails
|
||||
{
|
||||
Title = "Related data",
|
||||
Detail = "The operation is not allowed because related data exists."
|
||||
},
|
||||
_ => new ProblemDetails
|
||||
{
|
||||
Title = "Database error",
|
||||
Detail = $"An error occurred while saving data to the database. {postgresException.MessageText}"
|
||||
}
|
||||
};
|
||||
|
||||
logger.LogDebug("Database exception: {message}", context.Exception.CompleteExceptionMessage());
|
||||
context.ExceptionHandled = true;
|
||||
context.Result = new ObjectResult(problemDetails)
|
||||
{
|
||||
ContentTypes = { "application/problem+json" },
|
||||
StatusCode = StatusCodes.Status500InternalServerError
|
||||
};
|
||||
}
|
||||
}
|
||||
157
Events-MVC/Events.MVC/Util/TagHelpers/PagerTagHelper.cs
Normal file
157
Events-MVC/Events.MVC/Util/TagHelpers/PagerTagHelper.cs
Normal file
@@ -0,0 +1,157 @@
|
||||
using Events.MVC.Models;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using Microsoft.AspNetCore.Mvc.Routing;
|
||||
using Microsoft.AspNetCore.Mvc.ViewFeatures;
|
||||
using Microsoft.AspNetCore.Razor.TagHelpers;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Events.MVC.Util.TagHelpers;
|
||||
|
||||
[HtmlTargetElement("pager", Attributes = "page-info,page-action")]
|
||||
public class PagerTagHelper : TagHelper
|
||||
{
|
||||
private readonly IUrlHelperFactory urlHelperFactory;
|
||||
private readonly PagingSettings pagingSettings;
|
||||
|
||||
public PagerTagHelper(IUrlHelperFactory urlHelperFactory, IOptions<PagingSettings> pagingSettings)
|
||||
{
|
||||
this.urlHelperFactory = urlHelperFactory;
|
||||
this.pagingSettings = pagingSettings.Value;
|
||||
}
|
||||
|
||||
[ViewContext]
|
||||
[HtmlAttributeNotBound]
|
||||
public ViewContext ViewContext { get; set; } = null!;
|
||||
|
||||
public PagingInfo PageInfo { get; set; } = new();
|
||||
|
||||
public string PageAction { get; set; } = string.Empty;
|
||||
|
||||
public string PageTitle { get; set; } = "Unesite broj stranice";
|
||||
|
||||
public string? PageTarget { get; set; }
|
||||
|
||||
public string? PageSwap { get; set; }
|
||||
|
||||
public bool PagePushUrl { get; set; }
|
||||
|
||||
[HtmlAttributeName(DictionaryAttributePrefix = "page-route-")]
|
||||
public Dictionary<string, string> PageRouteValues { get; set; } = [];
|
||||
|
||||
public override void Process(TagHelperContext context, TagHelperOutput output)
|
||||
{
|
||||
output.TagName = "nav";
|
||||
output.Attributes.SetAttribute("aria-label", "Pager");
|
||||
|
||||
var paginationList = new TagBuilder("ul");
|
||||
paginationList.AddCssClass("pagination");
|
||||
paginationList.AddCssClass("mb-0");
|
||||
|
||||
var firstPageInRange = Math.Max(1, PageInfo.CurrentPage - pagingSettings.PageOffset);
|
||||
var lastPageInRange = Math.Min(PageInfo.TotalPages, PageInfo.CurrentPage + pagingSettings.PageOffset);
|
||||
|
||||
if (firstPageInRange > 1)
|
||||
{
|
||||
paginationList.InnerHtml.AppendHtml(BuildListItemForPage(1, "1.."));
|
||||
}
|
||||
|
||||
for (var page = firstPageInRange; page <= lastPageInRange; page++)
|
||||
{
|
||||
paginationList.InnerHtml.AppendHtml(
|
||||
page == PageInfo.CurrentPage
|
||||
? BuildListItemForCurrentPage(page)
|
||||
: BuildListItemForPage(page));
|
||||
}
|
||||
|
||||
if (lastPageInRange < PageInfo.TotalPages)
|
||||
{
|
||||
paginationList.InnerHtml.AppendHtml(BuildListItemForPage(PageInfo.TotalPages, $"..{PageInfo.TotalPages}"));
|
||||
}
|
||||
|
||||
output.Content.AppendHtml(paginationList);
|
||||
}
|
||||
|
||||
private TagBuilder BuildListItemForPage(int page)
|
||||
{
|
||||
return BuildListItemForPage(page, page.ToString());
|
||||
}
|
||||
|
||||
private TagBuilder BuildListItemForPage(int page, string text)
|
||||
{
|
||||
var urlHelper = urlHelperFactory.GetUrlHelper(ViewContext);
|
||||
var url = urlHelper.Action(PageAction, BuildRouteValues(page)) ?? string.Empty;
|
||||
|
||||
var anchor = new TagBuilder("a");
|
||||
anchor.InnerHtml.Append(text);
|
||||
anchor.Attributes["href"] = url;
|
||||
anchor.AddCssClass("page-link");
|
||||
ApplyHtmxAttributes(anchor, url);
|
||||
|
||||
var listItem = new TagBuilder("li");
|
||||
listItem.AddCssClass("page-item");
|
||||
listItem.InnerHtml.AppendHtml(anchor);
|
||||
return listItem;
|
||||
}
|
||||
|
||||
private TagBuilder BuildListItemForCurrentPage(int page)
|
||||
{
|
||||
var urlHelper = urlHelperFactory.GetUrlHelper(ViewContext);
|
||||
var urlTemplate = urlHelper.Action(PageAction, BuildRouteValues("__page__")) ?? string.Empty;
|
||||
|
||||
var input = new TagBuilder("input");
|
||||
input.Attributes["type"] = "text";
|
||||
input.Attributes["value"] = page.ToString();
|
||||
input.Attributes["data-current"] = page.ToString();
|
||||
input.Attributes["data-min"] = "1";
|
||||
input.Attributes["data-max"] = PageInfo.TotalPages.ToString();
|
||||
input.Attributes["data-url-template"] = urlTemplate;
|
||||
input.Attributes["title"] = PageTitle;
|
||||
if (!string.IsNullOrWhiteSpace(PageTarget))
|
||||
{
|
||||
input.Attributes["data-target"] = PageTarget;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(PageSwap))
|
||||
{
|
||||
input.Attributes["data-swap"] = PageSwap;
|
||||
}
|
||||
|
||||
input.Attributes["data-push-url"] = PagePushUrl.ToString().ToLowerInvariant();
|
||||
input.AddCssClass("page-link");
|
||||
input.AddCssClass("pagebox");
|
||||
|
||||
var listItem = new TagBuilder("li");
|
||||
listItem.AddCssClass("page-item");
|
||||
listItem.AddCssClass("active");
|
||||
listItem.InnerHtml.AppendHtml(input);
|
||||
|
||||
return listItem;
|
||||
}
|
||||
|
||||
private void ApplyHtmxAttributes(TagBuilder tagBuilder, string url)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(PageTarget))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
tagBuilder.Attributes["hx-get"] = url;
|
||||
tagBuilder.Attributes["hx-target"] = PageTarget;
|
||||
tagBuilder.Attributes["hx-swap"] = string.IsNullOrWhiteSpace(PageSwap) ? "outerHTML" : PageSwap;
|
||||
if (PagePushUrl)
|
||||
{
|
||||
tagBuilder.Attributes["hx-push-url"] = "true";
|
||||
}
|
||||
}
|
||||
|
||||
private RouteValueDictionary BuildRouteValues(object pageValue)
|
||||
{
|
||||
var routeValues = new RouteValueDictionary(PageRouteValues.ToDictionary(kvp => kvp.Key, kvp => (object?)kvp.Value));
|
||||
routeValues["page"] = pageValue;
|
||||
routeValues["pageSize"] = PageInfo.ItemsPerPage;
|
||||
routeValues["sorts"] = PageInfo.Sorts;
|
||||
routeValues["filters"] = PageInfo.Filters;
|
||||
return routeValues;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user