I have a simple razor component that accepts a comment from a user and then saves the comment. The Page that renders it is currently located at Pages >> Expeditions >> Index.cshtml. When I navigate to /Expeditions in a browser everything loads correctly and the OnValidSubmit works. When I navigate to /Expeditions/Index the page renders properly but the OnValidSubmit is never fired.
I'm guessing there is some type of magic that takes place when I leave out Index in the URL. I'm wondering what I am doing incorrectly here because if I put the component in any page other than an Index page, the Submit button doesn't fire the OnValidSubmit.
Here is the code...
Index.cshtml
#page
#model Project1.com.Pages.Expeditions.IndexModel
#{
ViewData["Title"] = "Home page";
}
#(await Html.RenderComponentAsync<ComposeCommentComponent>(RenderMode.ServerPrerendered, new { PostId = 1 }))
<script src="_framework/blazor.server.js"></script>
ComposeCommentComponent.razor
#using Microsoft.AspNetCore.Components.Web
#using Microsoft.AspNetCore.Components.Forms
#using Project1.com.Models
#using Project1.com.Services
#using System.Security.Claims
#using Microsoft.AspNetCore.Components.Authorization
#inject AuthenticationStateProvider AuthenticationStateProvider
#inject CommentService CommentService
<EditForm Model="#Comment" OnValidSubmit="OnValidSubmit">
<div class="form-group">
<InputTextArea id="Comment" #bind-Value="#Comment.Comment" class="form-control" rows="5" cols="65" placeholder="Leave a Comment!"></InputTextArea>
</div>
<button class="btn btn-primary float-right">Submit</button>
</EditForm>
#functions{
public ClaimsPrincipal User { get; set; }
protected override async void OnInitialized()
{
await base.OnInitializedAsync();
var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
User = authState.User;
}
}
#code {
[Parameter]
public int PostId { get; set; }
CommentModel Comment = new CommentModel();
private async void OnValidSubmit()
{
// Update Database with New Comment
CommentService.CreateComment(new CommentModel() { Username = User.Identity.Name, Comment=Comment.Comment, PostId=PostId});
// Clear Comment
Comment.Comment = "";
// Notify Parent Component to Update Data.
await OnNewComment.InvokeAsync(Comment.Id);
}
[Parameter]
public EventCallback<int> OnNewComment { get; set; }
}
Startup.cs
using Project1.com.Data;
using Project1.com.Services;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.UI;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Project1.com
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")));
services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores<ApplicationDbContext>();
services.AddRazorPages();
services.AddServerSideBlazor();
services.AddTransient<ExpeditionService>();
services.AddTransient<CommentService>();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
}
else
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapRazorPages().RequireAuthorization();
endpoints.MapBlazorHub();
});
}
}
}
If you check the page that doesn't work you'll find there are one or more blazor.server.js 404 errors. Probably this:
Your problem is that you are not specifying a base href for your application.
/Expeditions works because Blazor thinks you're in the root, but
/Expeditions/Index doesn't because it now tries to access resources from the /Expeditions subdirectory.
Blazor uses <base href="~/" /> to define where to get it's resources.
#page
#using StackOverflow.Web.Components
#model StackOverflowAnswers.Pages.MeModel
#{
ViewData["Title"] = "Home page";
}
<!DOCTYPE html>
<html lang="en">
<head>
<base href="~/" />
</head>
<body>
#(await Html.RenderComponentAsync<ComposeCommentComponent>(RenderMode.ServerPrerendered, new { PostId = 1 }))
<script src="_framework/blazor.server.js"></script>
</body>
</html>
Note: #enet's points in his answer still apply to clean up your code.
Related
I have a simple page that also includes a PartialView so that I can refresh the data every 10 seconds returned by the PartialView. It really is a trivial task but I'm not proficient enough with the whole web part and especially JavaScript.
How can I run the LoadData() Method every X seconds so that my partialView shows the data?
I have found the following JS code which I think should be able to refresh the PartialView but I don't know how to adapt this to my situation:
<script type="text/javascript">
$(function () {
setInterval(loadTable, 1000); // invoke load every second
loadTable(); // load on initial page loaded
});
function loadTable() {
$('#data').load('/controller/tabledata');
}
</script>
My index.cshtml looks like this:
#page
#model IndexModel
<div class="data">
<partial name="~/Pages/Shared/_BezoekersData.cshtml" />
</div>
The indexModel:
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.SharePoint.Client;
using PnP.Framework;
using System.Security;
namespace MyNameSpace.Pages
{
public class IndexModel : PageModel
{
public IActionResult OnGetPartial() => Partial("\\Shared\\_BezoekersData.cshtml");
// properties left out for clarity
public void OnGet()
{
LoadData();
}
public void LoadData()
{
// dataRetrieval part here.
}
}
}
_BezoekersData.cshtml:
#{
ViewData["Title"] = "Home page";
}
<div style="position:fixed; left:65px; top:25px; color:#c2a149; font-size:75px; ">#Model.Vestiging</div>
<div style="position:fixed; left:1100px; top:180px; color:#c2a149; font-size:50px">#Model.MaxAantal</div>
<div style="position:fixed; left:1100px; top:285px; color:#c2a149; font-size:50px">#Model.HuidigAantal</div>
You should add one more function to call the partial view and refresh it, check this:
public void OnGet()
{
LoadData();
}
public void LoadData()
{
MyModel=new MyModel {thetime=DateTime.Now };
}
public PartialViewResult OnGetMyPartial() // extra method to get data
{
MyModel = new MyModel { thetime = DateTime.Now };
return Partial("_BezoekersData", MyModel);
}
View:
Mind it must be ** div id="data"**, because you use #data to call jquery function.
Pass model data to partial, add model attribute.
And to call the new method, using handler:
In main view: #Model.MyModel.thetime
<div id="data">
<partial name="~/Pages/Shared/_BezoekersData.cshtml" model="Model.MyModel"/> //add a model attribute
</div>
#section Scripts
{
<script type="text/javascript">
$(function () {
setInterval(loadTable, 1000);
loadTable();
});
function loadTable() {
$('#data').load("/Index?handler=MyPartial"); //handler to call razor method
}
</script>
}
This is the partial view:
#model He621.Models.MyModel
In partial:<div style="position:fixed; left:65px; top:100px; color:#c2a149; font-size:75px; ">#Model.thetime</div>
Result:
The time refreshes every 1 seconds, and will only refresh the partial:
I made an RCL(Razor Class Library) for reusable UI.
Here is the code of front-end:
#model Sample.Models.VistorModel
#using Microsoft.AspNetCore.Mvc.Localization
#inject IViewLocalizer Localizer
#addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
<form id="ContactForm" asp-action="Contact">
<div>
<h3>#Localizer["Name"]</h3>
<span asp-validation-for="Name"></span>
</div>
<input asp-for="Name" placeholder="#Localizer["NameHint"]" />
<div>
<h3>#Localizer["Phone"]</h3>
<span asp-validation-for="Phone"></span>
</div>
<input asp-for="Phone" placeholder="#Localizer["PhoneHint"]" type="tel" />
<button type="submit">#Localizer["Sumit"]</button>
</form>
Here is the model:
public class VistorModel
{
[Required(ErrorMessageResourceName = "NameError", ErrorMessageResourceType = typeof(Sample.Resources.Views.Contact.Contact))]
public string Name { get; set; }
[Required(ErrorMessageResourceName = "PhoneError", ErrorMessageResourceType = typeof(Sample.Resources.Views.Contact.Contact))]
[RegularExpression(#"((\d{11})|^((\d{7,8})|(\d{4}|\d{3})-(\d{7,8})|(\d{4}|\d{3})-(\d{7,8})-(\d{4}|\d{3}|\d{2}|\d{1})|(\d{7,8})-(\d{4}|\d{3}|\d{2}|\d{1}))$)", ErrorMessageResourceName = "NotAPhoneNumber", ErrorMessageResourceType = typeof(Sample.Resources.Views.Contact.Contact))]
public string Phone { get; set; }
}
And here is the controller:
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Sample.Controllers
{
[Route("{culture}/[controller]")]
public class ContactController : Controller
{
[Route("Contact.html")]
public IActionResult Contact()
{
return View();
}
[HttpPost]
[ValidateAntiForgeryToken]
[Route("Contact.html")]
public IActionResult Contact(Models.VistorModel Vistor)
{
if (ModelState.IsValid)
{
}
return View();
}
}
}
For example,
the RCL URL is www.sample.com/contact/contact.html
the page URL is www.sample.com/home/index.html(the page is made by asp.net core also)
Now I embedded the RCL into the page.
However, when I click the submit button on the page, the page will redirect to the www.sample.com/contact/contact.html but not www.sample.com/home/index.html.
Besides, I need to embed this RCL not only on the home page but also on any other page.
In a word, what I want to do is:
1.When I input nothing(invalid), the page will redirect to www.sample.com/home/index.html and show the invalid error message on the span which has set attribute asp-validation-for before.
2.When I input name and phone(valid), the page will redirect to www.sample.com/home/index.html and show a message box alert that submits success.
Now I don't know how to make the URL redirect and display the invalid error message correctly. How can I achieve it? Thank you.
As far as I know, if you have set the asp-action, it will generate the form action with /contact/contact not home index.
To solve this issue, you should firstly move the RCL's view to the shared folder and then modify the form tag as below:
<form id="ContactForm" method="post">
The SDK I am using is 5.0.100-rc.2.20479.15.
The target framework also is .Net 5.
Here is my code in startup.cs:
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
namespace Sample
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
services.AddRazorPages();
services.AddServerSideBlazor();
services.AddHttpContextAccessor();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
}
app.UseStaticFiles();
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapBlazorHub();
endpoints.MapFallbackToPage("/_Host");
});
}
}
}
And here is my code in index.razor:
#page "/"
#inject IHttpContextAccessor httpContextAccessor
<h1>#IP</h1>
#code{
public string IP{get;set;}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
IP=httpContextAccessor.HttpContext.Connection?.RemoteIpAddress.ToString();
}
}
}
I published it to the server computer and the IP always returns null.
My program needs to submit a form. And the form is only allowed to submit once per day by the same people(without an account).
So I need to get the IP and set an IMemoryCache with a one-day DateTimeOffset.
In spite, storing the IP address is not a perfect way to solve my feature but it seems I can only do it like this.
The server-side of Blazor Server App communicates with the front-end (Browser) via SignalR, not HTTP. Thus the HttpContext object is not available in Blazor Server App, except on the initial request to the app, which is always an HTTP request. This is the only opportunity when you can access the HttpContext. Note that the _Host.cshtml file is a Razor Page file, so you can put some code in it that access the HttpContext directly, and get whatever data you want to read from the HttpContext, such as the Remote IP, etc. You can then pass the data retrieved to your Blazor SPA as parameters of the component tag helper present in the _Host.cshtml. Add code in the OnInitialized life-cycle method of the App component to get the data, as for instance define a property that gets the remote IP address of the current user. You can then store this data in the local storage for a later use
WARNING: AS #enet comment suggests this is not the recommended way to go about this in Blazor
In case it helps anyone... Here is my interpretation of #Melon NG's suggestion
In _Host.cshtml.cs
using AutoCheck.BlazorApp.Components;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace AutoCheck.BlazorApp.Pages
{
public class Host : PageModel
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IRememberMe _rememberMe;
public Host(IHttpContextAccessor httpContextAccessor, IRememberMe rememberMe)
{
_rememberMe = rememberMe;
_httpContextAccessor = httpContextAccessor;
}
public void OnGet()
{
_rememberMe.RemoteIpAddress = _httpContextAccessor.HttpContext.Connection.RemoteIpAddress;
}
}
}
RememberMe
using System.Net;
namespace AutoCheck.BlazorApp.Components
{
public interface IRememberMe
{
public IPAddress RemoteIpAddress { get; set; }
}
public class RememberMe : IRememberMe
{
public IPAddress RemoteIpAddress { get; set; }
}
}
About.razor
#page "/About"
#inject IRememberMe RememberMe
<h3>About</h3>
<table class="table table-striped">
<thead>
<td>Item</td>
<td>Value</td>
</thead>
<tr>
<td>Remote IP</td>
<td>#RememberMe.RemoteIpAddress</td>
</tr>
</table>
In ConfigureServices in Startup.cs
public void ConfigureServices(IServiceCollection services)
{
...
//services.AddSingleton<IRememberMe, RememberMe>();
services.AddScoped<IRememberMe, RememberMe>();
...
}
I am trying to call the list of employees from my blazor application using api. When I try to call the method from API from my service controller class , immediately it would be going to razor html without going to api controller. This is my code
My blazor project startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddRazorPages();
services.AddServerSideBlazor();
services.AddHttpClient<IEmployeeService, EmployeeService>(client =>
{
client.BaseAddress = new Uri("https://localhost:44300/");
});
}
EmployeeListBase.cs
public class EmployeeListBase : ComponentBase
{
[Inject]
public IEmployeeService EmployeeService { get; set; }
public IEnumerable<Employee> Employees { get; set; }
protected override async Task OnInitializedAsync()
{
Employees = (await EmployeeService.GetEmployees()).ToList();
}
}
Employee service class
public async Task<IEnumerable<Employee>> GetEmployees()
{
return await httpClient.GetJsonAsync<Employee[]>("api/employees");
}
But when I call httpClient.GetJsonAsync api , it will immediately going to EmployeeListBase razor html file without going API controller. API address is working fine . I copied and paste the http api address in postman , it is working perfect. But when I call through blazor app it is not going to api controller . When I debug Employees , the data would be null. Any help would be very appreciated
#page "/"
#inherits EmployeeListBase
<h3>Employee List</h3>
<div class="card-deck">
#foreach (var employee in Employees)
{
<div class="card m-3" style="min-width: 18rem; max-width:30.5%;">
<div class="card-header">
<h3>#employee.FirstName #employee.LastName</h3>
</div>
<img class="card-img-top imageThumbnail" src="#employee.PhotoPath" />
<div class="card-footer text-center">
View
Edit
Delete
</div>
</div>
}
</div>
Starting with an empty MVC project, here's my Startup.cs:
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
namespace AntiForgeryExample
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseDeveloperExceptionPage();
app.UseStatusCodePages();
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapDefaultControllerRoute();
});
}
}
}
And here's my HomeController.cs:
using Microsoft.AspNetCore.Mvc;
using System.Net;
namespace AntiForgeryExample
{
public class XyzController : Controller
{
[HttpPost]
[ValidateAntiForgeryToken]
public string Fgh() => "fgh 1";
[HttpGet]
public ContentResult Efg()
{
return new ContentResult()
{
ContentType = "text/html",
StatusCode = (int)HttpStatusCode.OK,
Content = #"<!DOCTYPE html>
<html>
<body>
<form method=""post"" action=""/Xyz/Fgh"">
<button type=""submit"">123</Button>
</form>
</body>
</html>"
};
}
}
}
The following line in Startup.cs adds the anti-forgery middleware:
services.AddMvc();
So, if we go to http://localhost:52838/Xyz/Efg, we get the simple page with a single button:
If we press the button, we get a 400 "Bad Request" response:
I'm assuming this is because we haven't passed a valid request verification token as part of the post. The Fgh method has the ValidateAntiForgeryToken attribute applied:
[HttpPost]
[ValidateAntiForgeryToken]
public string Fgh() => "fgh 1";
Thus a token is required when calling this method.
As described on this page, normally the code for this token will automatically be included if you use the form tag helper with ASP.NET Core MVC or a Razor Page. It will look something like this and be included as part of the form tag:
<input name="__RequestVerificationToken" type="hidden" value="CfDJ8NrAkS ... s2-m9Yw">
In the example program I show above, we're programmatically generating the HTML with the form.
My question is, is there a way to programmatically generate the required token from C#, without having to go through using an MVC view or Razor Page. The idea would be that we'd get the token value and then include the input tag:
<input name="__RequestVerificationToken" type="hidden" value="TOKEN VALUE HERE">
I shared this question on the /r/dotnet subreddit.
/u/kenos1 provided a very helpful answer there:
You can inject Microsoft.AspNetCore.Antiforgery.IAntiforgery and call GetTokens() on it.
Here’s the documentation: link
As he mentions there, we inject IAntiforgery at the XyzController constructor:
private IAntiforgery antiforgery;
public XyzController(IAntiforgery antiforgery_)
{
antiforgery = antiforgery_;
}
We call GetAndStoreTokens on the IAntiforgery instance that we injected:
var token_set = antiforgery.GetAndStoreTokens(HttpContext);
And finally, we use the resulting token in the generated HTML:
return new ContentResult()
{
ContentType = "text/html",
StatusCode = (int)HttpStatusCode.OK,
Content = string.Format(#"<!DOCTYPE html>
<html>
<body>
<form method=""post"" action=""/Xyz/Fgh"">
<button type=""submit"">123</Button>
<input name=""__RequestVerificationToken"" type=""hidden"" value=""{0}"">
</form>
</body>
</html>",
token_set.RequestToken)
};
Here is the controller file in its entirety:
using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using System.Net;
namespace AntiForgeryExample
{
public class XyzController : Controller
{
private IAntiforgery antiforgery;
public XyzController(IAntiforgery antiforgery_)
{
antiforgery = antiforgery_;
}
[HttpPost]
[ValidateAntiForgeryToken]
public string Fgh() => "fgh 1";
[HttpGet]
public ContentResult Efg()
{
var token_set = antiforgery.GetAndStoreTokens(HttpContext);
return new ContentResult()
{
ContentType = "text/html",
StatusCode = (int)HttpStatusCode.OK,
Content = string.Format(#"<!DOCTYPE html>
<html>
<body>
<form method=""post"" action=""/Xyz/Fgh"">
<button type=""submit"">123</Button>
<input name=""__RequestVerificationToken"" type=""hidden"" value=""{0}"">
</form>
</body>
</html>",
token_set.RequestToken)
};
}
}
}
The official ASP.NET Core 3.1 documentation mentions the GetAndStoreTokens method here.