4

I'm beginner in web designing with ASP.NET Core. I wrote a view component that has a form with some inputs related to a view model. One of these inputs is a file input (of the IFormFile datatype).

I want to submit this view model to an action of a controller (POST action), check the validity of model, return another view component if the model state is valid, and remain on this view component with this view model if model state is not valid.

This is my View Model: PricingViewModel.cs

public class PricingViewModel
{
    [Display(Name = "Select a file")]
    public IFormFile formFile { get; set; }

    [Display(Name = "ColumnCode")]
    [Required(ErrorMessage = "Enter {0} value, please")]
    public string colCode { get; set; }

    [Display(Name = "ColumnName")]
    [Required(ErrorMessage = "Enter {0} value, please")]         
    public string colName { get; set; }
}   

My View Component (controller): PricingComponent.cs

public class PricingComponent : ViewComponent
{
    public async Task<IViewComponentResult> InvokeAsync(PricingViewModel pricing)
    {               
        return await Task.FromResult((IViewComponentResult)View("PricingView", pricing));
    }
}

My View Component (view): PricingView.cshtml

<form class="text-left" method="post" enctype="multipart/form-data">

   <input name="IsValidPricing" type="hidden" value="@ViewBag.isValid" />

   <div class="form-group text-left">
     <label asp-for="colCode" class="control-label"></label>
     <input asp-for="colCode" class="form-control" id="colCodeId"/>
     <span asp-validation-for="colCode" class="text-danger"></span>
   </div>

   <div class="form-group text-left">
     <label asp-for="colName" class="control-label"></label>
     <input asp-for="colName" class="form-control" id="colNameId"/>
     <span asp-validation-for="colName" class="text-danger"></span>
   </div>

   <div class="form-group text-left">
     <label asp-for="formFile " class="control-label"></label>
     <input type="file" accept=".xlsx, .csv" asp-for="formFile" id="MyInputFile"/>
   </div>

   <div class="form-group mt-4">
     <input type="submit" asp-action="ShowPricing" asp-controller="Home" value="Show" id="ShowPricingBtn" />
   </div>
</form>

My Home Controller: HomeController.cs

[HttpPost]
public IActionResult ShowPricing(PricingViewModel pricing)
{
    if (ModelState.IsValid)
    {
        int temp;
        if (!int.TryParse(pricing.colCode, out temp))
        {
            ViewBag.isValid = 0;
            ModelState.AddModelError("colCode", "Invalid Data");
            return ViewComponent("PricingComponent", new { pricing = pricing }); // 1
        }
        else if (!int.TryParse(pricing.colName, out temp))
        {
            ViewBag.isValid = 0;
            ModelState.AddModelError("colName", "Invalid Data");
            return ViewComponent("PricingComponent", new { pricing = pricing }); //2
        }
        else
        {
            ViewBag.isValid = 1;   
            
            // do something ...

            return ViewComponent("ShowPricingExcelComponent"); //Call another view component
        }
    }
    else
    {
       ViewBag.isValid = 0;
       return ViewComponent("PricingComponent", new { pricing = pricing }); //3
    }
}

Plan A

The above approach is my primary plan.

Problem

If I use options of submit input tag (asp-action, asp-controller) like above, the view model sends correctly, but I don't know how to handle the validity of the model and remain on this view component. In the above code, when the ShowPricing action runs, if the model state is valid, the code works correctly, but when model is invalid (1,2,3), the PricingView doesn't show the validation summery, and just loads with current view model.

Plan B

I used AJAX to send the viewModel to the action and instead of showing the validation summary, I send an alert to the user with AJAX. I changed PricingView as following:

My View Component (view): PricingView.cshtml

<form class="text-left" method="post" enctype="multipart/form-data">

   <input name="IsValidPricing" type="hidden" value="@ViewBag.isValid" />

   <div class="form-group text-left">
     <label asp-for="colCode" class="control-label"></label>
     <input asp-for="colCode" class="form-control" id="colCodeId"/>
     <span asp-validation-for="colCode" class="text-danger"></span>
   </div>

   <div class="form-group text-left">
     <label asp-for="colName" class="control-label"></label>
     <input asp-for="colName" class="form-control" id="colNameId"/>
     <span asp-validation-for="colName" class="text-danger"></span>
   </div>

   <div class="form-group text-left">
     <label asp-for="fromFile " class="control-label"></label>
     <input type="file" accept=".xlsx, .csv" asp-for="formFile" id="MyInputFile"/>
   </div>

    <script>
      $(document).ready(function () {
        $('#ShowPricingBtn').click(function () {
                var _url = '@Url.Action("ShowPricing", "Home")';
                var input = $("#MyInputFile").get(0).files[0]; 

                $.ajax({
                    type: "POST",
                    url: _url,
                    data: {
                       formFile: input,
                       colCode: $("#colCode").val(),
                       colName: $("#colName").val(),
                    },
                    success: function (result) 
                    {
                       var IsValid = $('body').find('[name="IsValidPricing"]').val();
                       if (IsValid) 
                       {
                          $("#ShowExcelTable").html(result);
                       }
                       else {
                          alert("Invalid Data");
                       }
                    },
                });
           });
       });
    </script>
   <div class="form-group mt-4">
     <input type="submit" value="Show" id="ShowPricingBtn" />
   </div>
</form>

Problem

In this code:

  1. If the model state is not valid, the alert sends correctly, but
  2. If the model state is valid, the formFile input doesn't send correctly to action and it's null in view model.

I don't know whether I should go with the original or the alternate approach these problems. Do you know where I'm going wrong?

6
  • 1
    I've checked your code and it's working with validation. Errors are displayed. Why you are using return ViewComponent(...) instead of return View(...)? Commented Jan 19, 2021 at 6:53
  • @Justthebenno Which plan did you check? How did you write code that is worked and validation displayed. I used return viewComponent(...) because my view is a viewcomponent (in ~/Views/shared/Components). Commented Jan 19, 2021 at 6:59
  • 1
    I made some changes like always returning the same ViewComponent return ViewComponent("PricingComponent", new { pricing = pricing }); I wanted to avoid having new components for each case. I build a simple GET method like ``` [HttpGet] public IActionResult ShowPricing() => ViewComponent("PricingComponent", new { pricing = new PricingViewModel() });``` and send the form. Result: pasteboard.co/JKkcYTO.png Commented Jan 19, 2021 at 7:05
  • 1
    For your use-case, a simple view seems to be enough. ViewComponents are more for advanced scenarios Commented Jan 19, 2021 at 7:08
  • @Justthebenno I don't write all of my project. I just write a part of this. because of rest of my project, I have to use view component, not simple view. Commented Jan 19, 2021 at 7:23

2 Answers 2

5

Not sure how do you call view components,here are the working demos:

For PlanA

1.Create ViewComponents/PricingComponent.cs and ViewComponents/ShowPricingExcelComponent.cs.

public class PricingComponent : ViewComponent
{
    public async Task<IViewComponentResult> InvokeAsync(PricingViewModel pricing)
    {
        return await Task.FromResult((IViewComponentResult)View("PricingView", pricing));
    }
}    
public class ShowPricingExcelComponent : ViewComponent
{
    public async Task<IViewComponentResult> InvokeAsync(PricingViewModel pricing)
    {
        return await Task.FromResult((IViewComponentResult)View("ShowPricingExcel", pricing));
    }
}

2.Create Views/Shared/Components/PricingComponent/PricingView.cshtml.

@model PricingViewModel 
<form class="text-left" method="post" enctype="multipart/form-data">

    <input name="IsValidPricing" type="hidden" value="@ViewBag.isValid" />

    <div class="form-group text-left">
        <label asp-for="colCode" class="control-label"></label>
        <input asp-for="colCode" class="form-control" id="colCodeId" />
        <span asp-validation-for="colCode" class="text-danger"></span>
    </div>

    <div class="form-group text-left">
        <label asp-for="colName" class="control-label"></label>
        <input asp-for="colName" class="form-control" id="colNameId" />
        <span asp-validation-for="colName" class="text-danger"></span>
    </div>

    <div class="form-group text-left">
        <label asp-for="formFile " class="control-label"></label>
        <input type="file" accept=".xlsx, .csv" asp-for="formFile" id="MyInputFile" />
    </div>

    <div class="form-group mt-4">
        <input type="submit" asp-action="ShowPricing" asp-controller="Home" value="Show" id="ShowPricingBtn" />
    </div>
</form>

3.Create Views/Shared/Components/ShowPricingExcelComponent/ShowPricingExcel.cshtml.

<h1>Excel....</h1>

Project Structure:

enter image description here

4.Views/Home/Index.cshtml:

@await Component.InvokeAsync("PricingComponent")

5.HomeController:

public class HomeController : Controller
{       
    public IActionResult Index()
    {
        return View();
    }

    [HttpPost]
    public IActionResult ShowPricing(PricingViewModel pricing)
    {
        if (ModelState.IsValid)
        {
            int temp;
            if (!int.TryParse(pricing.colCode, out temp))
            {
                ViewBag.isValid = 0;
                ModelState.AddModelError("colCode", "Invalid Data");
                return View("Index", pricing);
            }
            if (!int.TryParse(pricing.colName, out temp))
            {
                ViewBag.isValid = 0;
                ModelState.AddModelError("colName", "Invalid Data");
                return View("Index", pricing);
            }
            else 
            {
                ViewBag.isValid = 1;

                // do something ...

                return ViewComponent("ShowPricingExcelComponent"); //Call another view component
            }              
        }
        else
        {
            ViewBag.isValid = 0;
            return View("Index", pricing); //3
        }
    }
}

Result:

enter image description here

For PlanB

1.Create ViewComponents/PricingComponent.cs and ViewComponents/ShowPricingExcelComponent.cs.

2.Create Views/Shared/Components/PricingComponent/PricingView.cshtml.

Firstly,it should be type="button" otherwise it will call twice to the backend.Secondly,what you did in ajax is not correct,more detailed explation you could refer to this answer.At last,you could not judge the modelstate by get the value of IsValidPricing value in your sucess function.Because the value you get is always be the data you first render the page,you cannot get the changed ViewBag value when ajax post back.

@model PricingViewModel
<form class="text-left" method="post" enctype="multipart/form-data">

    <input name="IsValidPricing" type="hidden" value="@ViewBag.isValid" />

    <div class="form-group text-left">
        <label asp-for="colCode" class="control-label"></label>
        <input asp-for="colCode" class="form-control" id="colCodeId" />
        <span asp-validation-for="colCode" class="text-danger"></span>
    </div>

    <div class="form-group text-left">
        <label asp-for="colName" class="control-label"></label>
        <input asp-for="colName" class="form-control" id="colNameId" />
        <span asp-validation-for="colName" class="text-danger"></span>
    </div>

    <div class="form-group text-left">
        <label asp-for="formFile " class="control-label"></label>
        <input type="file" accept=".xlsx, .csv" asp-for="formFile" id="MyInputFile" />
    </div>

    <div class="form-group mt-4">
        @*it should be type="button"*@
        <input type="button" value="Show" id="ShowPricingBtn" />
    </div>
</form>

<script src="~/lib/jquery/dist/jquery.min.js"></script>

<script>
      $(document).ready(function () {
        $('#ShowPricingBtn').click(function () {
            var _url = '@Url.Action("ShowPricing", "Home")';
            var input = $("#MyInputFile").get(0).files[0];

            var fdata = new FormData();
            fdata.append("formFile", input);
            $("form input[type='text']").each(function (x, y) {
                fdata.append($(y).attr("name"), $(y).val());
            });
            $.ajax({
                type: "POST",
                url: _url,
                data: fdata,
                contentType: false,   
                processData: false,
                success: function (result)
                {
                    console.log(result);
                    if (result==false)
                    {
                        alert("Invalid Data");
                    }
                    else {
                        $("#ShowExcelTable").html(result);

                    }
                },
            });
        });
  });
</script>

3.Create Views/Shared/Components/ShowPricingExcelComponent/ShowPricingExcel.cshtml.

<h1>Excel....</h1>

4.Views/Home/Index.cshtml:

@await Component.InvokeAsync("PricingComponent")
<div id="ShowExcelTable"></div>

5.HomeController:

public class HomeController : Controller
{       
    public IActionResult Index()
    {
        return View();
    }
    [HttpPost]
    public IActionResult ShowPricing(PricingViewModel pricing)
    {
        if (ModelState.IsValid)
        {
            int temp;
            if (!int.TryParse(pricing.colCode, out temp)|| !int.TryParse(pricing.colName, out temp))
            {
                ViewBag.isValid = 0;
                return Json(false);
            }
            else
            {
                ViewBag.isValid = 1;

                // do something ...
                return ViewComponent("ShowPricingExcelComponent"); //Call another view component
            }
        }
        else
        {
            ViewBag.isValid = 0;
            return Json(false);
        }
    }
}

Result: enter image description here

Sign up to request clarification or add additional context in comments.

1 Comment

Thanks a lot for your help... I used plan B (ajax) and got answer.
1

I'm not able to reproduce your error. Your code, as presented, works as expected. A validation message is displayed.

To make it a working example, I've added a GET method first.

[HttpGet]
public IActionResult ShowPricing() => ViewComponent("PricingComponent", new { pricing = new PricingViewModel() });

Open the URL Home/ShowPricing

Fill out the form.

enter image description here

Send the form. And the validation message is displayed.

enter image description here

12 Comments

There was a typo, I guess, else if (!int.TryParse(updatePricing.colName, out temp)) to else if (!int.TryParse(pricing.colName, out temp)) but yes, your code.
What I can over you, is to have a Live share session or something similar and I can have a look at your code and we can try to figure out why.
Yeah. Because it returns a ViewComponent and not a View.
It is possible. Though, your input file makes it a bit complicated. Have a look here as inspiration. Keep in mind, that you need to add your other properties like colCode to the form data (formData.append("colCode", <ValueOfColCode>);)
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.