What is the most efficient way of chaging the Blazor NavMenu according to the web page we are in? - dynamic

I was needing a way to display certain options on the Blazor NavMenu according to the webpage i was in.
There is a similar question on StackOverflow that helped me out archieving this
Exchange Data between Page and NavMenu in Blazor
But I wanted a specific topic on how to do this(Dynamic NavMenu according to the page) and i'll post my solution to get some feedback if this is the correct way of doing it and also to help others with the same doubt. The following article helped me a lot too:
https://chrissainty.com/3-ways-to-communicate-between-components-in-blazor/

So, my solution is the following.
First i created a class to manage the content of NavManeu and allow communicatin between the pages and NavMenu
public class NavMenuState
{
//The items for the NavMenu that will have the display name[0] and url[1] in a list of a string array
public List<string[]> MenuItems { get; private set; } = new();
//The last location that was clicked
public string Location { get; private set; } = "";
public event Action OnChange;
//Allow pages to set neu menu items for that page
public void SetNewMenu(List<string[]> newMenuItems)
{
MenuItems = newMenuItems;
NotifyStateChanged();
}
//Alow pages to add items to the menu
public void AddMenuItem(string[] newMenuItem)
{
MenuItems.Add(newMenuItem);
NotifyStateChanged();
}
//Triggred when clicked in a navmenu item
public void Navigation(string url)
{
//Check if the location if different, otherwise keep the same apearance
if (url!=Location)
{
Location = url;
List<string[]> vs = new();
string[] vs1 = new string[2];
vs1[0] = "Inicio";
vs1[1] = "";
vs.Add(vs1);
vs1 = new string[2];
vs1[0] = "Counter";
vs1[1] = "counter";
vs.Add(vs1);
vs1 = new string[2];
vs1[0] = "Fetchdata";
vs1[1] = "fetchdata";
vs.Add(vs1);
vs1 = new string[2];
vs1[0] = "Utilizador";
vs1[1] = "genericform/utilizador";
vs.Add(vs1);
MenuItems = vs;
NotifyStateChanged();
}
}
private void NotifyStateChanged() => OnChange?.Invoke();
//Creates the base menu to be displayed on the website first render
public NavMenuState()
{
Navigation("Inicio");
}
}
Then register this class as a singleton
public class Program
{
public static async Task Main(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
//Register as singleton
builder.Services.AddSingleton<NavMenuState>();
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
await builder.Build().RunAsync();
}
}
The NavMenu component. -> Every time I click a menu item it resets the NavMenu items(or tries to).
#using MecanicoAppSqlite.Shared
#inject NavMenuState NavState
#implements IDisposable
<div class="top-row pl-4 navbar navbar-dark">
<a class="navbar-brand" href="">MecanicoAppSqlite</a>
<button class="navbar-toggler" #onclick="ToggleNavMenu">
<span class="navbar-toggler-icon"></span>
</button>
</div>
<div class="#NavMenuCssClass" #onclick="ToggleNavMenu">
<ul class="nav flex-column">
#if (NavState.MenuItems != null)
{
#foreach (string[] menuItem in NavState.MenuItems)
{
<li class="nav-item px-3">
<NavLink class="nav-link" #onclick="()=>LocationChanged(menuItem[1])" href="#menuItem[1]">
<span class="oi oi-list-rich" aria-hidden="true"></span> #menuItem[0]
</NavLink>
</li>
}
}
</ul>
</div>
#code {
private bool collapseNavMenu = true;
private string NavMenuCssClass => collapseNavMenu ? "collapse" : null;
private void ToggleNavMenu()
{
collapseNavMenu = !collapseNavMenu;
}
protected override void OnInitialized()
{
NavState.OnChange += StateHasChanged;
}
public void Dispose()
{
NavState.OnChange -= StateHasChanged;
}
public void LocationChanged(string url)
{
NavState.Navigation(url);
}
}
Then on a page where I want to make changes to NavMenu I inject there the NavMenuState as like in NavMenu component and i can change the items on the OnInitialized method or on any other method
protected override void OnInitialized()
{
string[] vs1 = new string[2];
vs1[0] = "New Item";
vs1[1] = "editarentidade";
NavState.AddMenuItem(vs1);
}
public void SetAllNewMenuButtonClick()
{
List<string[]> vs = new();
string[] vs1 = new string[2];
vs1[0] = "Fim";
vs1[1] = "";
vs.Add(vs1);
vs1 = new string[2];
vs1[0] = "Contadeiro";
vs1[1] = "counter";
vs.Add(vs1);
vs1 = new string[2];
vs1[0] = "Meteorologia";
vs1[1] = "fetchdata";
vs.Add(vs1);
vs1 = new string[2];
vs1[0] = "users";
vs1[1] = "genericform/utilizador";
vs.Add(vs1);
NavState.SetNewMenu(vs);
}

Related

Sending cshtml view mails with mailkit

I am using ASP.NET Core and Mailkit to send emails. Take the following (Basic) code:
var bodyBuilder = new BodyBuilder();
bodyBuilder.HtmlBody = GetBody();
var m = new MimeMessage();
m.To.Add(new MailboxAddress("gurdip.sira#gmail.com"));
m.From.Add(new MailboxAddress("Sender Name", "gurdip.sira#gmail.com"));
string s = GetBody();
// m.Body = bodyBuilder.ToMessageBody();
m.Body = new TextPart(MimeKit.Text.TextFormat.Html) {Text = s};
using (var smtp = new MailKit.Net.Smtp.SmtpClient())
{
smtp.Connect("smtp.gmail.com", 587);
smtp.AuthenticationMechanisms.Remove("XOAUTH2");
smtp.Authenticate("gurdip.sira#gmail.com", "December5!");
smtp.Send(m);
}
The GetBody() method just reads a html document (streamreader).
What I'd like to do is use razor views and cshtml as my emails may contain dynamic content (e.g. an unknown sized collection of certain items).
I can't seem to find definitive documentation on how to do this. The idea is to then just read the cshtml view as plain html but resolve the razor syntax and model variables.
Anyone done anything like this?
One solution is from your controller to pass the content.
public void TestAction(){
var content = PartialView("your_partial_view").ToString();
your_SendEmailFunction(content)
}
So basically you use the partial view as a string that you pass as a content to your method.
Here is a simple demo based on jmal73's comment in Paris Polyzos' blog like below:
1.custom interface:
public interface IViewRenderService
{
Task<string> RenderToStringAsync(string viewName, object model);
}
2.implement interface:
using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Routing;
public class ViewRenderService : IViewRenderService
{
private readonly IRazorViewEngine _razorViewEngine;
private readonly ITempDataProvider _tempDataProvider;
private readonly HttpContext _httpContext;
public ViewRenderService(IRazorViewEngine razorViewEngine,
ITempDataProvider tempDataProvider,
IHttpContextAccessor httpContextAccessor)
{
_razorViewEngine = razorViewEngine;
_tempDataProvider = tempDataProvider;
_httpContext = httpContextAccessor.HttpContext;
}
public async Task<string> RenderToStringAsync(string viewName, object model)
{
var actionContext = new ActionContext(_httpContext, new RouteData(), new ActionDescriptor());
var viewEngineResult = _razorViewEngine.FindView(actionContext, viewName, false);
if (viewEngineResult.View == null || (!viewEngineResult.Success))
{
throw new ArgumentNullException($"Unable to find view '{viewName}'");
}
var view = viewEngineResult.View;
using (var sw = new StringWriter())
{
var viewDictionary = new ViewDataDictionary(new EmptyModelMetadataProvider(), new ModelStateDictionary());
viewDictionary.Model = model;
var tempData = new TempDataDictionary(_httpContext, _tempDataProvider);
var viewContext = new ViewContext(actionContext, view, viewDictionary, tempData, sw, new HtmlHelperOptions());
viewContext.RouteData = _httpContext.GetRouteData(); //set route data here
await view.RenderAsync(viewContext);
return sw.ToString();
}
}
}
3.read .cshtml file and return string:
public class HomeController : Controller
{
private readonly IViewRenderService _viewRenderService;
public HomeController(IViewRenderService viewRenderService)
{
_viewRenderService = viewRenderService;
}
public IActionResult Index()
{
var data = new Users()
{
UserId = 1
};
return View(data);
}
public async Task<IActionResult> Privacy()
{
var data = new Users()
{
UserId = 1
};
var result = await _viewRenderService.RenderToStringAsync("Home/Index", data);
return Content(result);
}
4.Index.cshtml:
#model Users
<form>
<label asp-for="UserId"></label>
<br />
<input asp-for="UserId" class="form-control" maxlength="4" />
<span asp-validation-for="UserId" class="text-danger"></span>
<input type="submit" value="create" />
</form>
5.Register service:
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddScoped<IViewRenderService, ViewRenderService>();

razor pages core local methods

Is it OK to call a method in the #functions{} section in razor pages directly from HTML? This seems to work fine and it's much easier than calling an API, but I was wondering if there is a downside to this (security, performance, etc)?
For example, in the code...
#functions {
public class Tickets: PageModel
{
public ApplicationDbContext _db { get; set; }
public Tickets(ApplicationDbContext db)
{
_db = db;
}
public void OnGet()
{
}
public string GetTickets(int Top) //--> THIS IS THE METHOD I AM CALLING
{
var data = _db.Tickets.OrderByDescending(x => x.CreatedAt).Take(Top);
var jdata = JsonConvert.SerializeObject(data.ToList());
return jdata;
}
}
}
And the HTML...
<div class="card alert-warning" v-for="ticket in tickets">
<div class="card-body">
<h4 class="card-title">{{ticket.TicketSubject}}</h4>
Card link
Another link
</div>
</div>
#section Scripts {
<script>
new Vue({
el: '#app',
data: {
dismissSecs: 5,
dismissCountDown: 5,
tickets: #Html.Raw(Model.GetTickets(100)), //-->THIS WORKS, BUT IT IS OK TO USE LIKE THIS?
xx: ''
}
})
</script>
}
It works because I believe the method is called when the page is being rendered and you have constant value being passed in. It would not automatically do something like update client-side without AJAX code being involved.
But to answer you question I think the 'more correct' approach is to set a binding property in the OnGet and reference that client-side
public class Tickets : PageModel
{
public ApplicationDbContext _db { get; set; }
public Tickets(ApplicationDbContext db)
{
_db = db;
}
[BindProperty]
public string TicketData { get; set; }
public void OnGet()
{
TicketData = GetTickets(100);
}
public string GetTickets(int Top)
{
var data = _db.Tickets.OrderByDescending(x => x.CreatedAt).Take(Top);
var jdata = JsonConvert.SerializeObject(data.ToList());
return jdata;
}
}

ASP.NET Core tag helper for conditionally adding a class to an element

In Asp.Net MVC we can add class conditionally as following code:
<div class="choice #(Model.Active?"active":"")">
</div>
How can do this by using tagHelper and by remove else part in condition.
Ability to add a conditional css class by following tagHelper provides.
this code like AnchorTagHelper asp-route-* for add route values acts.
[HtmlTargetElement("div", Attributes = ClassPrefix + "*")]
public class ConditionClassTagHelper : TagHelper
{
private const string ClassPrefix = "condition-class-";
[HtmlAttributeName("class")]
public string CssClass { get; set; }
private IDictionary<string, bool> _classValues;
[HtmlAttributeName("", DictionaryAttributePrefix = ClassPrefix)]
public IDictionary<string, bool> ClassValues
{
get {
return _classValues ?? (_classValues =
new Dictionary<string, bool>(StringComparer.OrdinalIgnoreCase));
}
set{ _classValues = value; }
}
public override void Process(TagHelperContext context, TagHelperOutput output)
{
var items = _classValues.Where(e => e.Value).Select(e=>e.Key).ToList();
if (!string.IsNullOrEmpty(CssClass))
{
items.Insert(0, CssClass);
}
if (items.Any())
{
var classes = string.Join(" ", items.ToArray());
output.Attributes.Add("class", classes);
}
}
}
in _ViewImports.cshtml add reference to taghelper as following
#addTagHelper "*, WebApplication3"
Use tagHelper in View:
<div condition-class-active="Model.Active" condition-class-show="Model.Display">
</div>
result for Active = true and Display = true is:
<div class="active show">
</div>
There's no default way to do what you're asking. You would have to write a TagHelper that did that logic for you. Aka
[HtmlTargetElement(Attributes = "asp-active")]
public class FooTagHelper : TagHelper
{
[HtmlAttributeName("asp-active")]
public bool Active { get; set; }
public override void Process(TagHelperOutput output, TagHelperContext context)
{
if (Active)
{
// Merge your active class attribute onto "output"'s attributes.
}
}
}
And then the HTML would look like:
<div class="choice" asp-active="Model.Active"></div>

ASP.NET Core RC2 TagHelperOutput.Content.Append changes

With RC1 you could construct a new TagBuilder, and just append that to the output of a custom TagHelper as such:
public override void Process(TagHelperContext context, TagHelperOutput output)
{
var indicator = new TagBuilder("span");
indicator.AddCssClass("indicator");
output.Content.Append(indicator);
}
That now fails with RC2, as TagHelperOutput.Content.Append() only accepts a string.
I can mess about with TagBuilder.WriteTo(), but that seems overly complex compared to how it was.
Is there a new way to construct new tags and append to the output that I've missed?
In RC2 you can use
output.Content.AppendHtml(tag);
Sample of menu tag helper:
view:
<ul>
<menu action="Index" controller="Home">Home page</menu>
<menu action="List" controller="Home">List</menu>
</ul>
MenuTagHelper.cs:
[HtmlTargetElement(Attributes = "controller, action")]
public class MenuTagHelper : TagHelper
{
public string Controller { get; set; }
public string Action { get; set; }
[ViewContext]
public ViewContext ViewContext { get; set; }
private readonly IUrlHelperFactory urlHelperFactory;
public MenuTagHelper(IUrlHelperFactory urlHelperFactory)
{
this.urlHelperFactory = urlHelperFactory;
}
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
var sb = new StringBuilder();
var urlHelper = urlHelperFactory.GetUrlHelper(ViewContext);
var url = urlHelper.Action(Action, Controller);
var text = (await output.GetChildContentAsync()).GetContent();
output.TagName = "li";
var a = new TagBuilder("a");
a.MergeAttribute("href", $"{url}");
a.MergeAttribute("title", text);
a.InnerHtml.Append(text);
var routeData = ViewContext.RouteData.Values;
var currentController = routeData["controller"];
var currentAction = routeData["action"];
if (string.Equals(Action, currentAction as string, StringComparison.OrdinalIgnoreCase) && string.Equals(Controller, currentController as string, StringComparison.OrdinalIgnoreCase))
output.Attributes.Add("class", "active");
output.Content.AppendHtml(a);
}
}

MVC 4 multiple buttons in form - why isn't this code working

I am trying to use a variation of the code from this page:
Multiple button in MVC
But everytime I click on the buttons it goes to the index actionresult method and not one of the button methods. Index is the view name but the button clicks are happening in a partial view called "P_NewPersonForm.cshtml"
P_NewPersonForm.cshtml
#using (Html.BeginForm())
{
<div id="divClaimType">
#Html.Label("Claim type:")
#Html.DropDownListFor(m => m.modelClaim.ClaimType, new List<SelectListItem>
{
new SelectListItem{ Text="Legal", Value = "Legal" },
new SelectListItem{ Text="Immigration", Value = "Immigration" },
new SelectListItem{ Text="Housing", Value = "Housing" }
})
</div>
<div id="divClaimStatus" style="padding: 5px;">
#foreach(var item in Model.LinkerStatusOfClaim)
{
#Html.Label("Claim status:")
#Html.DropDownListFor(m => m.LinkerStatusOfClaim[0].ClaimStatusID, new SelectList(Model.modelClaimStatus, "ClaimStatusID", "ClaimStatus"))
#Html.LabelFor(m => m.LinkerStatusOfClaim[0].Notes)
#Html.TextAreaFor(m => m.LinkerStatusOfClaim[0].Notes)
#Html.LabelFor(m => m.LinkerStatusOfClaim[0].StartDate)
#Html.TextBoxFor(m => m.LinkerStatusOfClaim[0].StartDate, new { #id = "datepicker", #Value = DateTime.Now, #readonly = true, Style = "background:#cccccc;" })
<br />
#Html.ValidationMessageFor(model => model.LinkerStatusOfClaim[0].StartDate)
<br />
}
<input type="submit" value="Add another status to this claim..." name="action:add"/>
<input type="submit" value="Delete status." name="action:remove"/>
#* #Ajax.ActionLink("Add another status to this claim...", "AddClaim", "Client", new AjaxOptions { HttpMethod = "POST"})*#
</div>
}
</div>
I have one button for adding to the collection of claims and another to remove one from the collection.
ClientController
public ActionResult Index()
{
var Model = new modelPersonClaim();
// Add one item to model collection by default
LinkerStatusOfClaim LinkerStatusOfClaim = new LinkerStatusOfClaim();
Model.LinkerStatusOfClaim.Add(LinkerStatusOfClaim);
DataLayer.RepositoryClient RC = new RepositoryClient();
Model.isValidModel = true;
RC.GetClaimTypes(Model, PersonTypes.NewPerson.ToString());
return View(Model);
}
[HttpPost]
public ActionResult P_NewPersonForm(modelPersonClaim Model)
{
DataLayer.RepositoryClient RC = new RepositoryClient();
RC.GetClaimTypes(Model, PersonTypes.NewPerson.ToString());
Model.isValidModel = ModelState.IsValid;
if (ModelState.IsValid)
{
RC.CreatePerson(Model);
Model.SuccessfulInsert = true;
Model.InsertString = "Person data has been successfully inserted into the database.";
if (Model.modelClaim.ClaimMade)
{
RC.CreateClaim(Model);
}
}
else
{
Model.SuccessfulInsert = false;
Model.InsertString = "Person data could not be inserted into the database. Missing key fields.";
}
return View("Index", Model);
}
[AttributeUsage(AttributeTargets.Method, AllowMultiple=false, Inherited = true)]
public class MultiButtonAttribute : ActionNameSelectorAttribute
{
public string Name { get; set; }
public string Argument { get; set; }
public override bool IsValidName(ControllerContext controllerContext, string actionName, MethodInfo methodInfo)
{
var isValidName = false;
var keyValue = string.Format("{0}:{1}", Name, Argument);
var value = controllerContext.Controller.ValueProvider.GetValue(keyValue);
if (value != null)
{
controllerContext.Controller.ControllerContext.RouteData.Values[Name] = Argument;
isValidName = true;
}
return isValidName;
}
}
[HttpPost]
[MultiButtonAttribute(Name = "action", Argument = "Add another status to this claim...")]
public ActionResult AddClaimStatus(modelPersonClaim Model)
{
Model.LinkerStatusOfClaim.Insert(Model.LinkerStatusOfClaim.Count, new LinkerStatusOfClaim());
return View("Index", Model);
}
[HttpPost]
[MultiButtonAttribute(Name = "action", Argument = "Delete status.")]
public ActionResult RemoveClaimStatus(modelPersonClaim Model)
{
// Can't remove IF only 1
if (Model.LinkerStatusOfClaim.Count == 1)
{
}
else
{
Model.LinkerStatusOfClaim.RemoveAt(Model.LinkerStatusOfClaim.Count);
}
return View("Index", Model);
}
When I click one the buttons it hits the public override bool IsValidName twice. Once for each button. But then because the action name is always index, it goes to the index method and not one of the button methods.
Does anyone have any ideas how to fix this?
Something is wrong with this part:
var keyValue = string.Format("{0}:{1}", Name, Argument);
var value = controllerContext.Controller.ValueProvider.GetValue(keyValue);
Your attribute is this:
[MultiButtonAttribute(Name = "action", Argument = "Add another status to this claim...")]
So in that case keyValue will become: "action:Add another status to this claim..." while your HTML states: <input type="submit" value="Add another status to this claim..." name="action:add"/>, so I think Argument in your attribute should be add.