受欢迎的博客标签

OVERVIEW

I've recently made the transition to start developing all my C# applications in Asp.Net Core 3.1 and I've found that there aren't a lot of examples out there that clearly explain some of the issues I've ran into. Of course, the same goes for Blazor, which is a really cool new framework that was released as part of Asp.Net Core 3.0. With Blazor, you're able to code interactive web UIs in C# instead of JavaScript (which also gives you the added benefit of utilizing existing .NET libraries) and the UI updates are handled over a SignalR connection. That's pretty cool, right? Personally, what I really like about it is that the code gets compiled with the application instead of just hanging out there in the browser console for everyone to access.

Due to the aforementioned reasons, I decided to put a blog together in hopes that it may help someone save time and frustration. I've been the one on the other end of the frustration plenty of times!

PREREQUISITES

Since Asp.Net Core 3.1 includes Blazor and even a simple startup project includes Bootstrap, my code examples rely on all 3 of these. I'm also using Visual Studio 2019, as Visual Studio 2017 doesn't support Core 3.1.

You're also going to need an email account that you can connect to via SMTP and a Google reCAPTCHA v3 account at https://www.google.com/recaptcha/intro/v3.html for your site. You'll set it up inside the Admin Console at https://www.google.com/recaptcha/admin.

LET'S GET STARTED

The first thing you're going to need to do is create a new project in Visual Studio 2019. If you're adding this to an existing project, you can skip over this. 

  • Select the Asp.Net Core Web Application project template and click the Next button.
  • Setup the Project name, Location, and Solution name however you like. I'm just calling it BlazorContactFormApp and it automatically changed the Solution name for me. I left the Location as the default location. Then click the Create button.
  • On the next screen, .NET Core and ASP.NET Core 3.1 should already be selected in the top drop down menus. Then select "Web Application (Model-View-Controller) in the center section. You can leave the other settings as default, since they aren't relevant to this post anyway. Now click "Create" to create the project.

STARTUP FILE

Now that the project is setup, the first thing we're going to want to do is open the Startup.cs file and make a few adjustments to enable Blazor.

Server Side Blazor doesn't register the HttpClient by default, so we need to add the following to the ConfigureServices method. So, add the following right after services.AddControllersWithView();

if (!services.Any(x => x.ServiceType == typeof(HttpClient)))
{
services.AddScoped<HttpClient>(s =>
{
var uriHelper = s.GetRequiredService<NavigationManager>();
return new HttpClient
{
BaseAddress = new Uri(uriHelper.BaseUri)
};
});
}

You'll likely notice some red squiggly lines yelling at you right away, due to some namespace names not being recognized. You'll need to add the following using directives to the top of the page.

using System.Net.Http;
using Microsoft.AspNetCore.Components;

We also need to add Server Side Blazor services, so add the following right after this code block (still inside the ConfigureServices method).

services.AddServerSideBlazor();

Now, in the Configure method (the next one down), scroll down to app.UseEndpoints and you're going to add a new endpoint. It should now look like this:

app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
endpoints.MapBlazorHub(); // This is what I just added.
});

CONTACT FORM MODEL

You'll need to add a class to the Models folder for your new Contact Form. Right click on the Models folder, select Add, then New Item. Class should be selected by default in the center of your screen, but select it if it isn't. Go down and rename it to ContactFormModel.cs, then click the Add button.

Add the following inside your new ContactFormModel class.

[Required]
[MaxLength(100)]
public string Name { get; set; }

[Required]
[EmailAddress]
public string Email { get; set; }

[Phone]
public string Phone { get; set; }

[Required]
public string Message { get; set; }

public string ReCaptcha { get; set; }

You're also going to need to add the following using directive to the top of the file for the data annotations.

using System.ComponentModel.DataAnnotations;

RAZOR COMPONENT

I'm going to admit something I did when I first started learning to use Blazor. I kept trying to add the @code block to my View...and it wasn't working, of course. I soon (but not real soon) realized this all has to happen within a Razor Component. So, let's get a component added to the project. 

I want to keep things sort of organized, so let's create a new folder by right clicking on the project a selecting Add, then New Folder. Name the folder Components, then right click on it and select Add, then New Item. Now, select the Razor Component item (the one with the cool purple flame) and name the component ContactComponent.razor. Now click the Add button.

Right away you'll notice the @code block in there that I was talking about earlier. This is where all the C# code is going to go that performs the UI interactions.

For the following using directives, you could create an imports file if you were going to use multiple components, but I'm just going to keep it simple and place them at the top of this component code. So, add the following to the top of the ContactComponent.razor file:

@using System.Net.Http
@using Newtonsoft.Json.Linq
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.JSInterop
@using Models;

Note: If your Models are in another project or location, you'll need to adjust the Models using directive to reference your Models properly.

You're going to notice the Newtonsoft assembly isn't recognized. Microsoft removed it in favor of the new System.Text.Json. However, I decided to use Newtonsoft, so you'll need to go to the NuGet package manager (Tools > NuGet Package Manager > Manage NuGet Packages for Solution) and get Newtonsoft.Json. You might need to search for it, but it's likely already showing in the list. Click on it, then  click the check box next to your project on the right side, then click the Install button below down below to add it.

You should now see the Newtonsoft assembly is properly referenced (the red squiggly line should be gone) in the ContactComponent.razor file.

The next thing we need to do is inject a couple more directives just after the using directives.

@inject IJSRuntime JSRuntime
@inject HttpClient Http

Simply put, we need to inject IJSRuntime so our component can communicate with JavaScript in the View, since you can't place your script tags in the component and we're going to need to retrieve our token from the Google reCAPTCHA API. If you're interested, you can find more information about IJSRuntime in a nice post by Chris Sainty at https://chrissainty.com/using-javascript-interop-in-razor-components-and-blazor/.

The HttpClient will be used to send the model to the controller, so the reCAPTCHA can be validated and the form can be sent.

Either rename title in the h3 element or remove it. I'm just going to rename it as follows:

<h3>Contact Form</h3>

Blazor Form

If you want to see a more detailed overview about Blazor forms and validation, check out this post by Daniel Roth and Luke Latham at https://docs.microsoft.com/en-us/aspnet/core/blazor/forms-validation?view=aspnetcore-3.1

Now you'll need to add in an EditForm component. You will see a lot of red squiggly lines, but those will go away later, when the necessary code is placed in the @code block. After the h3 element, add the following:

<EditForm Model="@_ContactFormModel" OnValidSubmit="OnSubmit">
<DataAnnotationsValidator />

<p>
<label for="Name">Name</label><br />
<InputText id="Name" @bind-Value="_ContactFormModel.Name" />
<ValidationMessage For="@(() => _ContactFormModel.Name)" />
</p>
<p>
<label for="Email">Email</label><br />
<InputText id="Email" @bind-Value="_ContactFormModel.Email" />
<ValidationMessage For="@(() => _ContactFormModel.Email)" />
</p>
<p>
<label for="Phone">Phone</label><br />
<InputText id="Phone" @bind-Value="_ContactFormModel.Phone" />
<ValidationMessage For="@(() => _ContactFormModel.Phone)" />
</p>
<p>
<label for="Message">Message</label><br />
<InputTextArea id="Message" @bind-Value="_ContactFormModel.Message" rows="3" />
<ValidationMessage For="@(() => _ContactFormModel.Message)" />
</p>

<div class="mt-2">
<button id="submitBtn" class="btn btn-primary @btnVisibility"
type="submit">Submit</button>
<div class="bg-primary text-light rounded p-2 @sendVisibility">
<i class="fa fa-cog fa-spin"></i> Sending
</div>
</div>
</EditForm>

I would typically style this up a bit more with Bootstrap by utilizing the "input-group" and related classes, but for the sake of keeping this more simple, I opted not to here. If it's something you're interested in doing, you can learn more about it at https://getbootstrap.com/docs/4.3/components/input-group/.

Now an alert needs added after the EditForm component, to display the success for failure message. Again, this will show some red squiggly lines until the @code block is filled in. Add the following after the EditForm component.

@if (displaySendAlert == true)
{
<div id="sendalert" class="alert @alertinfo mt-2">
<span class="@alerttextclass">@alerttext</span>
</div>
}

@code Block

Now it's time to start filling in the @code block. First we need to add the following properties:

@code{
private ContactFormModel _ContactFormModel = new ContactFormModel();
private bool displaySendAlert { get; set; } = false;
private string alertinfo { get; set; }
private string alerttextclass { get; set; }
private string alerttext { get; set; }
private string btnVisibility = "d-block";
private string sendVisibility = "d-none";
string google { get; set; }
}

Next, still within the @code block, we need to start adding some methods. Create a couple spaces after the 'google' property and add the following method:

protected override async void OnAfterRender(bool firstRender)
{
if (firstRender)
{
google = await JSRuntime.InvokeAsync<string>("runCaptcha");
StateHasChanged();
}
}

With this, after the component has rendered, it's going to use JS Interop to get the token value from some JavaScript that will be in the View that we're going to place the component in.

Next is the OnSubmit method that will run when the Submit button is pressed in the EditForm. Add the following after the OnAfterRender method:

private async Task OnSubmit()
{
_ContactFormModel.ReCaptcha = google;
btnVisibility = "d-none";
sendVisibility = "d-inline-block";
var results = await SubmitForm();

if (results != "failed")
{
var result = JObject.Parse(results);
if(result.ContainsKey("success") && (bool)result["success"] == true)
{
await ShowSuccessAlert();
_ContactFormModel.Name = string.Empty;
_ContactFormModel.Email = string.Empty;
_ContactFormModel.Phone = string.Empty;
_ContactFormModel.Message = string.Empty;
btnVisibility = "d-block";
sendVisibility = "d-none";
}
else if(result.ContainsKey("message"))
{
btnVisibility = "d-block";
sendVisibility = "d-none";
ShowErrorAlert((string)result["message"]);
}
}
else
{
btnVisibility = "d-block";
sendVisibility = "d-none";
ShowErrorAlert("There was a problem submitting the form.");
}
}

In here, you can see that the form is submitted (using a method I will add next) and it basically waits for the results, then adjusts the UI elements by changing some bootstrap classes. It's also using two other methods (ShowSuccessAlert and ShowErrorAlert), which I'll also add further down.

Add the following after the OnSubmit method that you just added:

private async Task<string> SubmitForm()
{
var json = Newtonsoft.Json.JsonConvert.SerializeObject(_ContactFormModel);
var stringContent = new StringContent(json, System.Text.Encoding.UTF8,"application/json");
var response = await Http.PostAsync("/Mailer/SendMessage", stringContent);

if (response.IsSuccessStatusCode)
{
var resultContent = response.Content.ReadAsStringAsync().Result;
return resultContent;
}
else
return "failed";
}

The SubmitForm method serializes _ContactFormModel into a JSON string, then using StringContent, it gets formatted in a way necessary to send it as HTTP content to the Controller. Then it basically waits for a response. If the HTTP response was successful, it returns with a result.

The final two methods in the @code block are for display the success or failure alerts. Add the following after the SubmitForm method:

private async Task ShowSuccessAlert()
{
displaySendAlert = true;
alertinfo = "alert-success";
alerttextclass = "text-success";
alerttext = "Sent Successfully";
}

private async void ShowErrorAlert(string message)
{
displaySendAlert = true;
alertinfo = "alert-danger";
alerttextclass = "text-danger";
alerttext = message;
}

That's all we need to do with the ContactComponet.razor component. Now we'll move on to the Controller.

CONTROLLER

Right click on the "Controllers" folder in the project, then Add, then Controller. Select "MVC Controller - Empty" and click the Add button. Now change the Controller name to MailerController and click the Add button.

In addition to the existing using directives, you'll need to add a few more to the top of the Controller.

using BlazorContactFormApp.Models;
using System.Net;
using System.Net.Mail;
using Newtonsoft.Json;

We're also going to need to put a new class in the Models folder to handle the reCAPTCHA response inside the Controller. Right-click on the Models folder in the project, then Add, then New Item. Name the new class ReCaptcha.cs and click the Add button. Insert the following properties into the new ReCaptcha class.

public string Success { get; set; }
public string Score { get; set; }
public string ChallengeTs { get; set; }
public string HostName { get; set; }

Now, back inside the Controller, you can delete the Index method from this file or rename it. This is the new SendMessage method that's needed in the Controller.

[HttpPost]
public IActionResult SendMessage([FromBody]ContactFormModel form)
{
if (form == null) return BadRequest();
if (ModelState.IsValid)
{
try
{
// Enter your email address and password
var credentials = new NetworkCredential("YourEmailAddressHere", "YourPasswordHere");

var mail = new MailMessage()
{
From = new MailAddress("YourEmailAddressHere"), // Enter your email address
Subject = "Website Inquiry",
Body = FormattedBody(form.Name, form.Email, form.Phone, form.Message)
};

mail.IsBodyHtml = true;
mail.To.Add(new MailAddress("YourEmailAddressHere")); // Enter your email address

// You may have to tweak these settings depending on your mail server's requirements
var client = new SmtpClient()
{
UseDefaultCredentials = false,
Host = "mail.somedomain.com", // Enter your mail server host
Credentials = credentials,
/* Port = 587,
EnableSsl = true */
};

if (!Validate(form.ReCaptcha))
{
throw new Exception("The submission failed the spam bot verification. If you have " +
"JavaScript disabled in your browser, please enable it and try again.");
}
else
{
client.Send(mail);
}

return Json(new { success = true, message = "Your message was successfully sent." });
}
catch (Exception ex)
{
return Json(new { success = false, message = ex.Message });
}
}
return BadRequest();
}

Be sure to fill in your email address, password, and mail server host information. Depending on the requirements for your SMTP mail server, you may need to adjust the SmtpClient settings. I commented out the Port and EnableSsl, but you may need them. 

Here are the last two methods you need to add to the Controller.

private string FormattedBody(string name, string email, string phone, string message)
{
var senderInfo = String.Format(
"<b>From</b>: {0}<br/><b>Email</b>: {1}<br/><b>Phone</b>: {2}<br/><br/>",
name, email, phone);
return senderInfo + message;
}

private bool Validate(string gResponse)
{
using (var client = new System.Net.WebClient())
{
try
{
// Enter your reCAPTCHA private key here
string secretKey = "YourGoogleSecretKeyHere";
var gReply = client.DownloadString(string.Format(
"https://www.google.com/recaptcha/api/siteverify?secret={0}&response={1}",
secretKey, gResponse));

var jsonReturned = JsonConvert.DeserializeObject<ReCaptcha>(gReply);
return (jsonReturned.Success.ToLower() == "true");
}
catch (Exception)
{
throw;
}
}
}

The first method is just used to format the message. The Validate method needs you to add your reCAPTCHA secret key that you get from Google when you sign up for a reCAPTCHA v3 account (as mentioned earlier). In short, the message is sent to the Controller and it attempts to validate with Google (it does all its magic to determine if it's a legitimate request), then it sends if it's validated or throws a generic error if it fails.

THE VIEW

The final piece is adding the Blazor component and a few scripts to the View! We'll just put these in the Index.cshtml file that was automatically placed in the Views Home folder when the application was created by Visual Studio. You can go ahead and remove the div that contains the Welcome heading and "Learn about" text if you want.

First, add the following to the top of the page, just under the opening razor code block.

<script src="https://www.google.com/recaptcha/api.js?render=YourGoogleReCaptchaSiteKeyHere"></script>
<link src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.11.2/css/fontawesome.min.css" type="text/css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.11.2/js/all.min.js"></script>

Be sure to add your reCAPTCHA Site Key in the first script. Next, add the Blazor component. The font awesome stylesheet and script is for the spinning gear icon that will animate after you click the submit button on the form.

<component type="typeof(BlazorContactFormApp.Components.ContactComponent)" render-mode="ServerPrerendered" />

If you named your application something different, you'll need to adjust the namespaces inside "typeof" that applies to your component. Here, the component was named ContactComponent and placed inside the Components folder. FYI: I did have problems getting the namespaces to a component to load once (as I was trying to enter it, Visual Studio just wasn't recognizing it), so I saved the project, restarted Visual Studio and it worked after the restart.

The last part is to add two more scripts to the bottom of the View. We'll put these in an @section code block. I would normally have done that with the previously mentioned scripts too, but it wasn't necessary to create the RenderSection in the head of the layout just for this example.

@section Scripts{
<script src="~/_framework/blazor.server.js"></script>
<script>
runCaptcha = function (actionName) {
return new Promise((resolve, reject) => {
grecaptcha.ready(function () {
grecaptcha.execute('YourGoogleReCaptchaSiteKeyHere', { action: 'BlazorContactForm' }).then(function (token) {
resolve(token);
});
});
});
};
</script>
}

Again, you'll need to add your reCAPTCHA site key in the second script. For simplicity, I just placed it directly in the code, but you could also assign the value to something like ViewData["ReCaptchaSiteKey"] in the Controller (maybe retrieving the value from the appsettings.json file or db) and use that to place the values in the view.

The blazor.server.js script is required for Blazor to work. You could place this in your _Layout.cshtml file if you're going to be using components on several pages, but I try to only place it on pages that are actually using a Blazor component. Otherwise, there's a very brief connection error that will display between each page. Perhaps there's a way to delay that or something, but I haven't looked into it yet.

If you run the application, you should see the simple form displaying. If you click submit without filling in any fields, it will display some alerts about the required fields. If you fill them out and submit the form, it switches to a spinning icon until a response returns and it all happens seamlessly. Very cool!