How to dynamically switch child content in blazor pages - asp.net-core

As I'm an absoulute beginner when it comes to web development, I started to look Blazor and learn how to use it to get an easy start in to web developlment and now struggle with a problem.
I have built a Master / Detail page and that page uses a master component (the list of employees) and 2 different detail component (employee readonly detail view and employee edit view).
The master detail page uses the following routes:
https://localhost:44344/masterdetail
https://localhost:44344/masterdetail/{id:int}
https://localhost:44344/masterdetail/{id:int}/edit
I tried to accomplish these goals:
When a user clicks a list entry from the master component, this should be shown in the URL like https://localhost:44344/masterdetail/2 and than load the employee readonly detail view into the detail area
When a user clicks the edit button located on the employee readonly detail view, the master detail page should switch to the employee edit view inside the detail area and show this in the URL like https://localhost:44344/masterdetail/2/edit
When a user clicks the save button located on the employee edit view, the master detail page should switch to the employee readonly detail view inside the detail area and show this in the URL like https://localhost:44344/masterdetail/2
The problems that I have faced:
When the user is in the readonly view and than clicks the edit button, my code is calling NavigationManager.NavigateTo($"/masterdetail/{Id}/edit"); which switches the URL in the address bar of the browser but does not invoke the OnParametersSet() lifecycle method of the master detail page.
Blazor seems to reuse the instance if the [Parameter] Id has not changed it's value.
The same happens when the user is on /masterdetail/{Id}/edit route (entered via browser address bar) and than clicks the save button.
What I learned while researching the problem:
I know that I could use the forceLoad parameter of the
NavigationManager.NavigateTo($"/masterdetail/{Id}/edit", true);
call like this, but this would lead to a complete page refresh
and I'm not sure if this is necessary.
I know that I could use EventCallback<T> in my child components and
react on these events in the parent master detail page but this seems
like a workaround.
I looked for a way to "route inside a blazor page" and stumbled
across topics like "Areas" and "Partial Views" but it looks
like these are MVC concepts.
I also found something called the "RouteView"
(https://github.com/aspnet/AspNetCore/blob/2e4274cb67c049055e321c18cc9e64562da52dcf/src/Components/Components/src/RouteView.cs)
which is a Blazor component but I had no luck using it for my
purposes.
Here is a simplified sample that shows the problem:
Create a new "Blazor App" project in Visual Studio
Choose "Blazor Server App"
Add a new .razor file and paste the code snippet in
Have a look at the comments and the code
Navigate to https://localhost:44344/masterdetail/ and try it yourself
#*Default route for this page when no entry is selected in the master list*#
#page "/masterdetail"
#*Route for this page when an entry is selected in the master list. The detail area should show a readonly view / component*#
#page "/masterdetail/{id:int}"
#*Route for this page when an entry is selected in the master list and the user clicked the edit button in the readonly view / component. The detail area should show a edit view / component*#
#page "/masterdetail/{id:int}/edit"
#using Microsoft.AspNetCore.Components
#inject NavigationManager NavigationManager
<h1>MyMasterDetailPage</h1>
<br />
<br />
<br />
<div>
<h1>Master Area</h1>
<ul class="nav flex-column">
<li class="nav-item px-3">
<button #onclick=#(mouseEventArgs => ShowListItemDetails(1))>Item 1</button>
</li>
<li class="nav-item px-3">
<button #onclick=#(mouseEventArgs => ShowListItemDetails(2))>Item 2</button>
</li>
<li class="nav-item px-3">
<button #onclick=#(mouseEventArgs => ShowListItemDetails(3))>Item 3</button>
</li>
</ul>
</div>
<br />
<br />
<br />
<div>
<h1>Detail Area</h1>
#{
if (_isInEditMode)
{
// In the real project a <EmployeeEditComponent></EmployeeEditComponent> is being used here instead of the h2
<h2>Edit view for item no. #Id</h2>
<h3>Imagine lots of editable fields here e.g. TextBoxes, DatePickers and so on...</h3>
<button #onclick=#SaveChanges> save...</button>
}
else
{
// In the real project a <EmployeeDetailComponent></EmployeeDetailComponent> is being used here instead of the h2
<h2>ReadOnly view for item no. #Id</h2>
<h3>Imagine lots of NON editable fields here. Probably only labels...</h3>
<button #onclick=#SwitchToEditMode> edit...</button>
}
}
</div>
#code {
private bool _isInEditMode;
[Parameter]
public int Id { get; set; }
protected override void OnParametersSet()
{
// This lifecycle method is not called if the [Parameter] has already been set as Blazor seems to reuse the instance if the [Parameter] Id has not changed it's value.
// For example this method is not being called when navigating from /masterdetail/1 to /masterdetail/1/edit
Console.WriteLine($"Navigation parameters have been set for URI: {NavigationManager.Uri}");
_isInEditMode = NavigationManager.Uri.EndsWith("edit");
base.OnParametersSet();
}
private void ShowListItemDetails(int id)
{
Console.WriteLine($"Showing readonly details of item no. {id}");
NavigationManager.NavigateTo($"/masterdetail/{id}");
}
private void SwitchToEditMode()
{
Console.WriteLine("Switching to edit mode...");
NavigationManager.NavigateTo($"/masterdetail/{Id}/edit");
// Setting _isInEditMode = true here would work and update the UI correctly.
// In the real project this method is part of the <EmployeeEditComponent></EmployeeEditComponent> and therefore has no access to _isInEditMode as it belongs to the <MyMasterDetailPage> component.
// I know that I could create a public EventCallback<MouseEventArgs> OnClick { get; set; } in the <EmployeeEditComponent> and react to that event here in the <MyMasterDetailPage> component but is that really the right way to do this?
//_isInEditMode = true;
}
private void SaveChanges()
{
Console.WriteLine("Saving changes made in edit mode and switching back to readonly mode...");
NavigationManager.NavigateTo($"/masterdetail/{Id}");
// Setting _isInEditMode = false here would work and update the UI correctly.
// In the real project this method is part of the <EmployeeDetailComponent></EmployeeDetailComponent> and therefore has no access to _isInEditMode as it belongs to the <MyMasterDetailPage> component
// I know that I could create a public EventCallback<MouseEventArgs> OnClick { get; set; } in the <EmployeeDetailComponent> and react to that event here in the <MyMasterDetailPage> component but is that really the right way to do this?
//_isInEditMode = false;
}
}
My setup:
Visual Studio 2019 16.3.1
.NET Core 3.0 SDK - Windows x64 Installer (v3.0.100)
What is the best practice / recommendation on how to switch child content inside a blazor page?

I asked the question on the AspNetCore Github repo and got an answer.
https://github.com/aspnet/AspNetCore/issues/16653
As "mrpmorris" said, I changed the following lines
Before #page "/masterdetail/{id:int}/edit"
After #page "/masterdetail/{id:int}/{displayMode}"
Before -
After [Parameter]<br> public string DisplayMode { get; set; }
Before _isInEditMode = NavigationManager.Uri.EndsWith("edit");
After string.Equals(DisplayMode, "edit", StringComparison.InvariantCultureIgnoreCase);
and the website behaves as intended and that solves my problem :)

Related

BotDetect and ASPNET Razor Pages is not validating

I have decided to use BotDetect Captcha in my project to stop spam, however, I have not been able to check if the user has entered the correct captcha since Razor Pages doesn't support Filters.
On their site, they say to use this attribute to check if the captcha is valid
[CaptchaValidationActionFilter("CaptchaCode", "ExampleCaptcha", "Wrong Captcha!")]
However, razor pages doesn't allow attributes on page methods.
Digging into the source code of the attribute, I found this
MvcCaptcha mvcCaptcha = new MvcCaptcha(this.CaptchaId);
if (mvcCaptcha.IsSolved) { }
However when I tried that code directly in the OnPost method, mvcCaptch.IsSolved always returns false.
Checking the session variables also shows all of the BDC_ values required for this control to work so I've hit a wall here. Hoping someone could help me out. Thanks.
Official docs if it helps, although, I could'nt find any reference to Razor Pages on the site https://captcha.com/mvc/mvc-captcha.html
I found there is an attribute CaptchaModelStateValidation attribute you can apply to a Razor page model property that is bound to the captcha code input. This way you get the validation automatically in the ModelState.
Here is a sample model that validates the captcha.
public class CaptchaValidatorModel : PageModel
{
public void OnPost()
{
if (ModelState.IsValid)
{
// Perform actions on valid captcha.
}
}
[BindProperty]
[Required] // You need this so it is not valid if the user does not input anything
[CaptchaModelStateValidation("ExampleCaptcha")]
public string CaptchaCode { get; set; }
}
The page uses the code provided in the documentation sample.
#page
#model CaptchaWebApplication.Pages.CaptchaValidatorModel
#{
ViewData["Title"] = "Captcha";
}
<form method="post">
<label asp-for="CaptchaCode">Retype the code from the picture:</label>
<captcha id="ExampleCaptcha" user-input-id="CaptchaCode" />
<div class="actions">
<input asp-for="CaptchaCode" />
<input type="submit" value="Validate" />
<span asp-validation-for="CaptchaCode"></span>
#if ((HttpContext.Request.Method == "POST") && ViewData.ModelState.IsValid)
{
<span class="correct">Correct!</span>
}
</div>
</form>

Razor Pages Tag Helper with Dynamic Parameters Assigning Current Url as Href

I'm running into trouble trying to set anchor tag helper parameters dynamically and looking for some help.
I have a nav that is a view component inside the shared _Layout.cshtml page that populates departments from a model.
#model List<DepartmentModel>
<ul class="nav">
#foreach (var d in Model)
{
<li>
<a asp-page="catalog/departments" asp-route-departmentName="#d.Name" asp-route-departmentId="#d.Id">#d.Name</a>
</li>
}
</ul>
Here is the InvokeAsync() from my View Component class
public async Task<IViewComponentResult> InvokeAsync()
{
var departments = _catalogService.GetNavDepartments();
return View(departments);
}
When I first launch the page, all the hrefs are populating correctly.
"catalog/departments/department-name-1/department-id-1"
"catalog/departments/department-name-2/department-id-2"
"catalog/departments/department-name-3/department-id-3"
If I click on one of the links, like the first link for example, I go to the proper department page "catalog/departments/department-name-1/department-id-1"
However, once I click that first link and navigate to the respective page, all the nav hrefs populate to the current url "catalog/departments/department-name-1/department-id-1" instead of the originally generated hrefs. This makes it so I can't navigate to another department.
Here is my route in the Startup.cs
services.AddRazorPages().AddRazorPagesOptions(options => {
options.Conventions.AddPageRoute("/Catalog/Departments", "{dName}/{dId}");
});
Based on the convention above, it eliminates the "catalog/department" piece of the url but I added it in this description for a sense of what I'm trying to accomplish. Even if I add this template to the page that populates the "catalog/departments" url, I get the same result.
#page "{dName}/{dId}"
Can anyone help me figure out what I am missing? Thanks in advance!
******** UPDATE ********
Currently, the only way I am able to get this to work is by adding the cache tag helper.
<cache>
<ul class="nav">
#foreach (var d in Model)
{
<li>
<a asp-page="catalog/departments" asp-route-departmentName="#d.Name" asp-route-departmentId="#d.Id">#d.Name</a>
</li>
}
</ul>
</cache>
This doesn't seem like a proper fix. Seems more of a hack then anything. Anybody have any ideas? Thanks!

How to integrate my code into a sitefinity project

We have a sitefinity Customer Portal. Now we need to add MVC pages to it. I understand how to add a page, and how to drag e.g. a list to the page's content. But I don't understand how I can create a controller and other c# code to populate the list and do other custom things. We cannot open the project in Visual Studio, and we have no access to the existing code.
First of all, you must sure your project run success on your local. You can check it by login to back end page.
Then you can create the MVC component like this: (you should create all of this in root/MVC folder)
Create controller first:
[ControllerToolboxItem(Name = "ImportCSV", Title = "ImportCSV", SectionName = "ImportCSV")]
public class ImportCSVController : Controller
{
// GET: ImportCSV
public ActionResult Index()
{
return View();
}
}
SectionName is title of content group for you custom
Title is the title of component
Name is used for code behind
Then you can create the views to show in page: (you have to create the views in MVC/Views/ImportCSV, sitefinity will recognize folder name to map in BE)
<h2>Upload File</h2>
<div class="form-group">
<input type="file" id="dataFile" name="upload" />
</div>
<div class="form-group">
<a onclick="upload()" class="button" id="btnupload">Upload</a>
</div>
You need to get access to the code then, controllers\models need to be compiled. You can get away with a lot directly in a cshtml file though which DOESN'T need to be compiled.
Could you download a new blank SF project that's on your version and start from scratch pointed at your DB? Copy over /App_Data and /ResourcePackages to the new project and just run it. Should work fine, but any page that has a custom widget on it that uses custom code would tank. Sorry I'm just not sure why you don't have the code. Could use JustDecompile to retrieve the actual code for custom widgets too I suppose.

how to show all the errors on a page in one shot (submit button) by client side validation in mvc .net core

I have a page with many required fields. So when I click submit button required validation is firing for the first field then the second then the third and so on...
What I need to do here is , When I click on submit I have to show all errors on a page in one shot.
My requirement is to achieve this only by validating client side.
I am using an .Net core MVC application.
Below is the screenshot of my page
Can I achieve this.. Please help me..
Thanks !!
I can give you an idea to do your job using jquery custom validation.Please refer my solution.
Add custom style class to your required fields.
Example :
<input type="text" class="req-cls" >
Write Jquery function to Check Validation
$(document).ready(function () {
$('#btn1').click(function (e) {
var isValid = true;
$('.req-cls').each(function () {
if ($.trim($(this).val()) == '') {
isValid = false;
$(this).css({
"border": "1px solid red",
"background": "#FFCECE"
});
}
else {
$(this).css({
"border": "",
"background": ""
});
}
});
if (isValid == false)
e.preventDefault();
});
});
See Example here : https://jsfiddle.net/Shalitha/q2n8L9wg/24/
Just add this line in your .cshtml
<div class="validation-summary-valid" data-valmsg-summary="true">
<ul>
<li style="display: none;"></li>
</ul>
</div>
Since you need client side we are talking about JS. But with razor you can validate a few results using the model annotations. For example let's say you have this object.
public class UserCreationVO
{
[Required]
[StringLength(255)]
public string Username { get; set; }
}
Now what you need to do in your frontend (meaning your .cshtml file) is to tell asp.net to use this properties to validate. So for example:
#model UserCreationVO
<form method="post">
<input asp-for="UserName" />
<span asp-validation-for="UserName"></span>
</form>
As you can see above using asp-for is a great way to create validations using your models. Be careful you must pass as a model the object you want to validate. The asp-for tag shows a model property. So you can't pass it in a Viewbag or something. This produces some automatic html and js for you and handles it.
Furthermore you should always validate the result nevertheless in the controller. Because client side validation is for performance reasons and user experience and doesn't offer any kind of security:
public IActionResult CreateUser(UserCreationVO user)
{
if(!ModelState.IsValid)
return your_error;
}
Last but not least: You must include the JQuery unobtrusive validation library. Furthermore if you have some extra requirements like checking if a username exists (Which can't be done without contacting the server) then you can use the [Remote] attribute.
More info and reading about front-end validation with razor: here
How to use a remote attribute: Using remote validation with ASP.NET Core
EDIT:
So generally I advise to use models and create them. As you say policy is required in one form but not in another. What you should do to have a maintanable code where you simply change the attribute of your model and the validation happens you need to create a different VO. For example:
public class CreatePolicyVO
{
[Required]
public string PolicyNumber {get; set;}
}
And another object for example updating:
public class UpdatePolicyVO
{
public string PolicyNumber {get; set;}
}
Because you also need to validate them in the controller. So passing a different object allows you to use ModelState.IsValid and other MVC and razor features. Generally if a field is required in one case and not in another then you need a different model.
First, we need to add the JQuery,jquery.validate & jquery.validate.unobtrusive in our views.
<script src="https://ajax.aspnetcdn.com/ajax/jquery/jquery-2.2.0.min.js"></script>
<script src="https://ajax.aspnetcdn.com/ajax/jquery.validate/1.16.0/jquery.validate.min.js"></script>
<script src="https://ajax.aspnetcdn.com/ajax/jquery.validation.unobtrusive/3.2.6/jquery.validate.unobtrusive.min.js"></script>
Then in View add required data-* attributes like:
<label for="Name">Name</label>
<input type="text" data-val="true" data-val-length="Length must be between 10 to 25" data-val-length-max="25" data-val-length-min="10" data-val-required="Please enter the name" id="Name" name="Name" value="" />
<span class="field-validation-valid" data-valmsg-for="Name" data-valmsg-replace="true"></span>
<br />
You could see that it has added the several attributes starting with data-*.
The data-* attributes are part of the HTML5, which allow us the add extra information (metadata) to the HTML element.
The Javascript unobtrusive library reads the data-val attributes and performs the client side validation in the browser when the user submits the form. These Validations are done before the form is sent over an HTTP. If there is a validation error, then the request will not be sent.

Sitecore MVC Controller and Forms - ActionResult not rendering Layout on postback

Sitecore 7.1v1, most recent Glass Mapper, MVC4. When we submit the form POST, we get no layout with the return View. I'd prefer not to have to redirect to another page since this is supposed to be a wizard-like experience. This is also lightweight enough not to require Ajax, although we could use it as a last resort. I can't find who to make sure that while returning the View that we get the layout as well. I'm new to Sitecore MVC and pretty new at MVC in general. The PageBase that's referenced is a Custom Model using Glass.
We have the following Controller Rendering:
public class RegistrationController : Controller
{
[HttpGet]
public ActionResult VerifyAccount()
{
return View("~/Views/Public/Wizards/Registration/VerifyAccount.cshtml",
new SitecoreContext().GetCurrentItem<PageBase>());
}
[HttpPost]
public ActionResult CreateProfile()
{
ViewBag.Name = Request["VerificationType"];
ViewBag.Step = 2;
return View("~/Views/Public/Wizards/Registration/CreateProfile.cshtml",
new SitecoreContext().GetCurrentItem<PageBase>());
}
}
The default action for this is VerifyAccount(). This renders as expected. The initial view is as follows:
#inherits Glass.Mapper.Sc.Web.Mvc.GlassView<Public.Model.GlassModel.Primary.PageBase>
<div>
<h3>#Editable(a => a.Title)</h3>
<p>
#Editable(a => a.Description)
</p>
<hr />
#using (Html.BeginRouteForm(Sitecore.Mvc.Configuration.MvcSettings.SitecoreRouteName, FormMethod.Post))
{
#Html.Sitecore().FormHandler("Registration", "CreateProfile")
#Html.ValidationSummary(true, "Verification Not Selected.")
<fieldset>
#Sitecore.Globalization.Translate.Text("$Registration.VerificationTitle")
#{ var validations = new SitecoreContext().GetItem<GlassFrameBase>(Guid.Parse("{3694FC43-3DB7-470A-A1E9-2649856AAF10}"));}
<select id="VerType" name="VerificationType">
#foreach (var validation in validations.GetChildren<VerificationMethod>())
{
<option value="#validation.MethodValue">#validation.MethodName</option>
}
</select>
<input type="submit" value="Next" />
</fieldset>
}
This posts back to the CreateProfile() Method. This part works great. The only issue is that when it returns the view this time, it returns just the view without the layout.
The final view is as follows:
#using (Html.BeginRouteForm(Sitecore.Mvc.Configuration.MvcSettings.SitecoreRouteName, FormMethod.Post))
{
#Html.Sitecore().FormHandler()
<p>
<b>Verification Type Was: </b>#ViewBag.Name
</p>
<p>#ViewBag.Step</p>
<input type="hidden" value="ThisIsATest" name="TestHidden" id="TestHidden"/>
<input type="submit" name="back" value="Back" /><br />
<input type="submit" name="next" value="Next" />
}
Everything else is working exactly as expected but I'm missing something important that loads the Layout on the return trip.
I have noticed this before as well and I think it relates to this line:
#Html.Sitecore().FormHandler("Registration", "CreateProfile")
It seems to bypass the standard rendering pipeline and just call the target action. I have written a blog post on how you can control calls to different action on multiple controllers. this might help:
http://www.experimentsincode.com/?p=425
Try changing the return type of CreateProfile from ActionResult to PartialViewResult, and then return View("... to return PartialView("...
Also, here's a post about what you can return for Sitecore Controller Renderings.
http://mhwelander.net/2014/04/09/sitecore-controller-rendering-action-results-what-can-i-return/
I haven't looked deeply into form posting with Controller Renderings, but if the above suggestion doesn't work then maybe consider the execution lifestyle used in Sitecore MVC (mentioned in the post).