Issue with Handling POST Requests in CMS Upgrade: Converting BlockControllers to BlockComponents

Vote:
 

Hi,

I’m seeking clarification on converting Controllers to Components with the CMS upgrade from version 11 to 12. 
One aspect of this conversion involves changing BlockControllers to BlockComponents.

While the GET call for the Block Component works and the block loads as expected, the POST call does not work. My application has a few POST requests that is made via AJAX.

I understand that ViewComponents are primarily used for display purposes. However, I’m unsure how to handle post-back calls 
with ViewComponents, as using IActionResult within ViewComponents does not seem to work. BlockControllers are no longer valid in Optimizely12 .As a result, I ended up using both ViewComponents for display and an MVC Controller for handling post-back calls. 
 
Here are the issues I’ve encountered with this approach

1- When the AJAX call is fired, the block is null on the POST call. 

       

2- Next, I switched to using just a ViewModel in the Controller instead of the block. 

     The ViewModel has a content ID that I fetch in the GET call and pass in the form data from AJAX.This ID i include in VIewModel which gives me the block on POST method call as below:

using EPiServer;
using EPiServer.Core;
using EPiServer.DataAbstraction;
using EPiServer.Logging;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json.Linq;
using System.Net.Mail;
using EPiServer.Web.Mvc;
using XXX.Cryptography;
using XXX.EpiServer.Business.Attributes;
using XXX.EpiServer.Business.Messaging;
using XXX.EpiServer.Helpers;
using XXX.EpiServer.Models.Blocks;
using XXX.EpiServer.Models.ViewModels;
using XXX.EpiServer.SpecializedProperties;

namespace XXX.EpiServer.Controllers
{
    public class SecureEmailBlockController : Controller
    {
        private readonly IMessageSender _messageSender;
        private readonly IContentRepository _contentRepository;
        private readonly IContentTypeRepository _contentTypeRepository;
        private readonly IHttpContextAccessor _httpContextAccessor;
        private readonly IConfiguration _configuration;
        private readonly IContentLoader _contentLoader;
        private static readonly EPiServer.Logging.ILogger _logger = LogManager.GetLogger();

        public SecureEmailBlockController(IMessageSender messageSender, IContentRepository contentRepository, IContentTypeRepository contentTypeRepository, IConfiguration configuration, IHttpContextAccessor httpContextAccessor, IContentLoader contentLoader)
        {
            _messageSender = messageSender;
            _contentRepository = contentRepository;
            _contentTypeRepository = contentTypeRepository;
            _configuration = configuration;
            _httpContextAccessor = httpContextAccessor;
            _contentLoader = contentLoader;
        }
        [HttpPost]
        [Route("/api/public/{controller}/{action}")]
        [ValidateAntiForgeryToken]
        public IActionResult Index(SecureEmailBlock currentContent ,SecureEmailViewModel viewModel)
        {
            var messageAttachmentsAreValid = true;
            Attachment[] messageAttachments = null;

            int decryptedId = Convert.ToInt32(AesHmac.SimpleDecryptWithPassword(viewModel.EncryptedBlockId, _configuration["EncryptionPassword"]));
            var contentLink = new ContentReference(decryptedId);
            var currentBlock = _contentRepository.Get<SecureEmailBlock>(contentLink); 

            var secureEmailViewModel = GetSecureEmailViewModel(currentBlock, _contentRepository, _contentTypeRepository, viewModel);

            if (!ModelState.IsValid)
            {
                HttpContext.Response.StatusCode = 400;
                return View("SecureEmailBlock", secureEmailViewModel);
            }

            var selectedEmailRecipient = secureEmailViewModel.EmailRecipients.FirstOrDefault(recipient => recipient.FullName.Equals(viewModel.Recipient));
            if (selectedEmailRecipient == null)
            {
                HttpContext.Response.StatusCode = 400;
                return View("SecureEmailBlock", secureEmailViewModel);
            }

            _messageSender.SendMessage(new[] { new MailAddress(selectedEmailRecipient.EmailAddress) }, new MailAddress(viewModel.SenderEmailAddress, ($"{viewModel.SenderName}")),
                viewModel.Subject, false, viewModel.Body, messageAttachments);

            ModelState.Clear();

            return View("SecureEmailBlock", secureEmailViewModel);
        }

        private SecureEmailViewModel GetSecureEmailViewModel(SecureEmailBlock currentContent, IContentRepository contentRepository, IContentTypeRepository contentTypeRepository, SecureEmailViewModel viewModel)
        {
            var recipients = Enumerable.Empty<EmailRecipient>();
            var blockContent = (IContent)currentContent;

            var referenceInformation = contentRepository.GetReferencesToContent(blockContent.ContentLink, false).FirstOrDefault();
            if (referenceInformation != null)
            {
                var owner = contentRepository.Get<IContent>(referenceInformation.OwnerID);

                var ownerType = contentTypeRepository.Load(owner.ContentTypeID);
                var emailRecipientContentAreaAttribute = Attribute.GetCustomAttribute(ownerType.ModelType, typeof(EmailRecipientContentAreaAttribute)) as EmailRecipientContentAreaAttribute;

                if (emailRecipientContentAreaAttribute != null)
                {
                    var contactSearchItemContentAreaValue = emailRecipientContentAreaAttribute.ContactSearchItemContentArea;
                    var contactSearchItemContentArea = owner.Property[contactSearchItemContentAreaValue].Value as ContentArea;
                    var contactSearchItems = ContentHelpers.GetContentAreaBlocks<ContactSearchItemBlock>(contactSearchItemContentArea);

                    recipients = contactSearchItems.Select(contactSearchItem => new EmailRecipient
                    {
                        FirstName = contactSearchItem.FirstName,
                        MiddleName = contactSearchItem.MiddleName,
                        LastName = contactSearchItem.LastName,
                        EmailAddress = contactSearchItem.EmailAddress
                    });
                }
            }

            if (currentContent.Recipients != null)
            {
                recipients = recipients.Concat(currentContent.Recipients);
            }

            var fullNamedRecipients = recipients.Select(recipient =>
            {
                recipient.FullName = string.IsNullOrWhiteSpace(recipient.MiddleName) ? $"{recipient.FirstName} {recipient.LastName}" : $"{recipient.FirstName} {recipient.MiddleName} {recipient.LastName}";
                return recipient;
            });

            var sortedRecipients = (currentContent.SortRecipientsBy == "first, last, middle"
                ? fullNamedRecipients.OrderBy(recipient => recipient.FirstName)
                    .ThenBy(recipient => recipient.LastName)
                    .ThenBy(recipient => recipient.MiddleName)
                : fullNamedRecipients.OrderBy(recipient => recipient.LastName)
                    .ThenBy(recipient => recipient.FirstName)
                    .ThenBy(recipient => recipient.MiddleName));
            return new SecureEmailViewModel()
            {
                Recipient = Request.Query["recipient"],
                Body = viewModel == null ? "" : viewModel.Body,
                SendButtonText = currentContent.SendButtonText,
                ResetButtonText = currentContent.ResetButtonText,
                FormHeading = currentContent.Heading,
                SenderName = viewModel == null ? "" : viewModel.SenderName,
                SenderEmailAddress = viewModel == null ? "" : viewModel.SenderEmailAddress,
                Subject = viewModel == null ? "" : viewModel.Subject,
                EmailRecipients = sortedRecipients.ToList(),
                SiteKey = currentContent.SiteKey,
                SecretKey = currentContent.SecretKey,
                ReCaptchaScoreThreshold = currentContent.ReCaptchaScoreThreshold
            };
        }        
    }
}

However, this approach results in the block instance being different, causing model validation failures and issues with returning the view (return View("SecureEmailBlock", viewModel);).

System.InvalidOperationException: The view 'SecureEmailBlock' was not found. The following locations were searched:
Views/ElementBlocks/SecureEmailBlock.cshtml
/CmsDefault/Views/ElementBlocks/SecureEmailBlock.cshtml
~/Views/Shared/ElementBlocks/SecureEmailBlock.cshtml
/CmsDefault/Views/Shared/ElementBlocks/SecureEmailBlock.cshtml
/FormsViews/Views/ElementBlocks/SecureEmailBlock.cshtml
/CmsDefault/FormsViews/Views/ElementBlocks/SecureEmailBlock.cshtml
/Views/SecureEmailBlock/SecureEmailBlock.cshtml
/CmsDefault/Views/SecureEmailBlock/SecureEmailBlock.cshtml
/Views/Shared/SecureEmailBlock.cshtml
/CmsDefault/Views/Shared/SecureEmailBlock.cshtml
/Pages/Shared/SecureEmailBlock.cshtml
/CmsDefault/Pages/Shared/SecureEmailBlock.cshtml
/Util
/CmsDefault/Util
~/Views/Shared/Blocks/SecureEmailBlock.cshtml
/CmsDefault/Views/Shared/Blocks/SecureEmailBlock.cshtml
~/Views/Shared/Media/SecureEmailBlock.cshtml
/CmsDefault/Views/Shared/Media/SecureEmailBlock.cshtml
~/Views/Shared/PagePartials/SecureEmailBlock.cshtml
/CmsDefault/Views/Shared/PagePartials/SecureEmailBlock.cshtml

Here's the Folder structure that i have where the Block is placed:

Additonal info on the Modal State from 11 vs 12

Any advice or suggestions on how to handle POST calls with ViewComponents or alternative approaches to resolve these issues would be greatly appreciated.

Regards,

Farhin

#329779
Edited, Sep 12, 2024 19:24
ZZ
Vote:
 

Hi,

Why don't you use PageController for POST requests as viewcomponents are mainly used for display purpose.

I can see in error logs that Razor view file is not found /Views/Shared/SecureEmailBlock.cshtml, where have you placed your view file ? 

#330165
Sep 18, 2024 10:24
Farhin - Sep 18, 2024 12:50
I have updated my query above with additional details about the POST request and the folder structure. I’ve used a Controller for handling POST requests since I know that ViewComponents are primarily for display purposes. It’s only after adding the Controller class that the POST requests are being fired. However, even with the Controller, the block is empty.
I’m curious about how Optimizely 12 handles POST requests because right now, the Model.IsValid check is failing since the block is empty. If I pass the Content ID in the model and assign it in the GET request (which is what I’m currently doing), then on the POST request, using the same ID to retrieve the block from the Content Repository causes the model validation to fail because the instance of the block is not the same.
I hope I’m making sense here.
ZZ - Sep 18, 2024 13:05
Can you share the controller class ?
Farhin - Sep 18, 2024 13:17
Updated the query with the complete Controller class
ZZ
Vote:
 

OK. Try to start with a simple method for debugging purpose. 

In below code replace "Hardcoded value" with the Id of a block you want to fetch and see if you get the relevant block

using EPiServer;
using EPiServer.Core;
using EPiServer.DataAbstraction;
using EPiServer.Logging;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json.Linq;
using System.Net.Mail;
using EPiServer.Web.Mvc;
using XXX.Cryptography;
using XXX.EpiServer.Business.Attributes;
using XXX.EpiServer.Business.Messaging;
using XXX.EpiServer.Helpers;
using XXX.EpiServer.Models.Blocks;
using XXX.EpiServer.Models.ViewModels;
using XXX.EpiServer.SpecializedProperties;

namespace XXX.EpiServer.Controllers
{
    public class SecureEmailBlockController : Controller
    {
        private readonly IMessageSender _messageSender;
        private readonly IContentRepository _contentRepository;
        private readonly IContentTypeRepository _contentTypeRepository;
        private readonly IHttpContextAccessor _httpContextAccessor;
        private readonly IConfiguration _configuration;
        private readonly IContentLoader _contentLoader;
        private static readonly EPiServer.Logging.ILogger _logger = LogManager.GetLogger();

        public SecureEmailBlockController(IMessageSender messageSender, IContentRepository contentRepository, IContentTypeRepository contentTypeRepository, IConfiguration configuration, IHttpContextAccessor httpContextAccessor, IContentLoader contentLoader)
        {
            _messageSender = messageSender;
            _contentRepository = contentRepository;
            _contentTypeRepository = contentTypeRepository;
            _configuration = configuration;
            _httpContextAccessor = httpContextAccessor;
            _contentLoader = contentLoader;
        }
        [HttpPost]
        [Route("/api/public/{controller}/{action}")]
        [ValidateAntiForgeryToken]
        public IActionResult Index(SecureEmailViewModel viewModel)
        {
            var messageAttachmentsAreValid = true;
            Attachment[] messageAttachments = null;

            int decryptedId = Convert.ToInt32(AesHmac.SimpleDecryptWithPassword(viewModel.EncryptedBlockId, _configuration["EncryptionPassword"]));
            var contentLink = new ContentReference(decryptedId);
            //var currentBlock = _contentRepository.Get<SecureEmailBlock>(contentLink);

            // For testing purpose only
            var currentBlock = _repo.Get<BlockData>(new ContentReference("Hardcoded value"));

            var secureEmailViewModel = GetSecureEmailViewModel(currentBlock, _contentRepository, _contentTypeRepository, viewModel);

            if (!ModelState.IsValid)
            {
                HttpContext.Response.StatusCode = 400;
                return View("SecureEmailBlock", secureEmailViewModel);
            }

            var selectedEmailRecipient = secureEmailViewModel.EmailRecipients.FirstOrDefault(recipient => recipient.FullName.Equals(viewModel.Recipient));
            if (selectedEmailRecipient == null)
            {
                HttpContext.Response.StatusCode = 400;
                return View("SecureEmailBlock", secureEmailViewModel);
            }

            _messageSender.SendMessage(new[] { new MailAddress(selectedEmailRecipient.EmailAddress) }, new MailAddress(viewModel.SenderEmailAddress, ($"{viewModel.SenderName}")),
                viewModel.Subject, false, viewModel.Body, messageAttachments);

            ModelState.Clear();

            return View("SecureEmailBlock", secureEmailViewModel);
        }

        private SecureEmailViewModel GetSecureEmailViewModel(SecureEmailBlock currentContent, IContentRepository contentRepository, IContentTypeRepository contentTypeRepository, SecureEmailViewModel viewModel)
        {
            var recipients = Enumerable.Empty<EmailRecipient>();
            var blockContent = (IContent)currentContent;

            var referenceInformation = contentRepository.GetReferencesToContent(blockContent.ContentLink, false).FirstOrDefault();
            if (referenceInformation != null)
            {
                var owner = contentRepository.Get<IContent>(referenceInformation.OwnerID);

                var ownerType = contentTypeRepository.Load(owner.ContentTypeID);
                var emailRecipientContentAreaAttribute = Attribute.GetCustomAttribute(ownerType.ModelType, typeof(EmailRecipientContentAreaAttribute)) as EmailRecipientContentAreaAttribute;

                if (emailRecipientContentAreaAttribute != null)
                {
                    var contactSearchItemContentAreaValue = emailRecipientContentAreaAttribute.ContactSearchItemContentArea;
                    var contactSearchItemContentArea = owner.Property[contactSearchItemContentAreaValue].Value as ContentArea;
                    var contactSearchItems = ContentHelpers.GetContentAreaBlocks<ContactSearchItemBlock>(contactSearchItemContentArea);

                    recipients = contactSearchItems.Select(contactSearchItem => new EmailRecipient
                    {
                        FirstName = contactSearchItem.FirstName,
                        MiddleName = contactSearchItem.MiddleName,
                        LastName = contactSearchItem.LastName,
                        EmailAddress = contactSearchItem.EmailAddress
                    });
                }
            }

            if (currentContent.Recipients != null)
            {
                recipients = recipients.Concat(currentContent.Recipients);
            }

            var fullNamedRecipients = recipients.Select(recipient =>
            {
                recipient.FullName = string.IsNullOrWhiteSpace(recipient.MiddleName) ? $"{recipient.FirstName} {recipient.LastName}" : $"{recipient.FirstName} {recipient.MiddleName} {recipient.LastName}";
                return recipient;
            });

            var sortedRecipients = (currentContent.SortRecipientsBy == "first, last, middle"
                ? fullNamedRecipients.OrderBy(recipient => recipient.FirstName)
                    .ThenBy(recipient => recipient.LastName)
                    .ThenBy(recipient => recipient.MiddleName)
                : fullNamedRecipients.OrderBy(recipient => recipient.LastName)
                    .ThenBy(recipient => recipient.FirstName)
                    .ThenBy(recipient => recipient.MiddleName));
            return new SecureEmailViewModel()
            {
                Recipient = Request.Query["recipient"],
                Body = viewModel == null ? "" : viewModel.Body,
                SendButtonText = currentContent.SendButtonText,
                ResetButtonText = currentContent.ResetButtonText,
                FormHeading = currentContent.Heading,
                SenderName = viewModel == null ? "" : viewModel.SenderName,
                SenderEmailAddress = viewModel == null ? "" : viewModel.SenderEmailAddress,
                Subject = viewModel == null ? "" : viewModel.Subject,
                EmailRecipients = sortedRecipients.ToList(),
                SiteKey = currentContent.SiteKey,
                SecretKey = currentContent.SecretKey,
                ReCaptchaScoreThreshold = currentContent.ReCaptchaScoreThreshold
            };
        }
    }
}
#330175
Sep 18, 2024 13:43
Farhin - Sep 18, 2024 14:38
No difference in the output when converting to BlockData instead of the specific Block Type. But the one difference that I see when comparing the same with 11 for ModelState is attached in the query above.
ZZ - Sep 18, 2024 15:18
But do you get exact block type ? You can insert breakpoints and see the output
ZZ - Sep 18, 2024 15:22
Try this when returning view ->
return View("~/Views/Shared/Blocks/Components/SecureEmailBlock/SecureEmailBlock.cshtml", secureEmailViewModel);

if this doesn't work then share some of html from SecureEmailBlock.cshtml
Farhin - Sep 18, 2024 15:23
Yes I do get the exact Block. Just needed conversion in GetSecureEmailViewModel
var secureEmailViewModel = GetSecureEmailViewModel(currentBlock, _contentRepository, _contentTypeRepository, viewModel);
ZZ - Sep 18, 2024 15:36
Do you have got any particular line of code which is thorwing an error ?
Farhin - Sep 18, 2024 16:24
Model.IsValid is the first place where the validation fails.The other is return View() as it’s not able to relocate the view.I get that issue mentioned might be case by case scenario but
What I am really trying to know is how are post requests being handled in Optimizely12 since BlockControllers are obsolete in 12.
ZZ - Sep 19, 2024 7:26
Yes because your Model state is not valid. You have to look at the view model and compare it to the data sent from browseren. I have created all my POST requests in page controllers and not MVC controller. POST requests are part of MVC architecture.
https://learn.microsoft.com/en-us/aspnet/core/tutorials/first-mvc-app/controller-methods-views?view=aspnetcore-6.0
* You are NOT allowed to include any hyperlinks in the post because your account hasn't associated to your company. User profile should be updated.