2

I have view with tasks. Each task has an @Ajax.Action link to add this task to check list. My view:

@foreach (var item in Model.Tasks) {
    <tr>
        <td>
            @Html.DisplayFor(modelItem => item.TaskText)
        </td>
        <td>
            @Html.DisplayFor(modelItem => item.TillDate)
        </td>
        <td>
            @Html.EnumDropDownListFor(modelItem => item.State, new { id=item.Id, @class="state"})
        </td>
        <td>
            @Html.ActionLink("Edit", "Edit", new { id=item.Id }) |
            @Html.ActionLink("Delete", "Delete", new { id = item.Id })
            @Ajax.ActionLink("Add To check list", "AddToCheckList", new { id = item.Id }, new AjaxOptions { UpdateTargetId = "CheckListPartial" });
        </td>
    </tr>
}

My controller action:

    public PartialViewResult AddToCheckList(int id)
    {
        context.AddTaskToCheckList(id);
        return PartialView("_LoadCheckList", context.CheckList);
    }

And CheckList class:

public class CheckList
{
    public string Name { get; set; }
    public List<Task> Tasks { get; set; } = new List<Models.Task>();
}

Now adding works, but I have a problem: I can add one task to check list several times. How can I validate if check list contains task and show error message?

UPD: I've make this with my controller. Validation works, but message is not shown.

    public PartialViewResult AddToCheckList(int id)
    {
        if(context.CheckList.Tasks.Exists(t=>t.Id==id))
            ModelState.AddModelError("CheckList", "Check list already contains this task.");

        if (ModelState.IsValid)
        {
            context.AddTaskToCheckList(id);
            return PartialView("_LoadCheckList", context.CheckList);
        }
        return PartialView();
    }

Also add this string to my View:

@Html.ValidationMessageFor(model => model.CheckList)

UPD2: My PartialView:

@model TaskTracker.Models.CheckList

<p>
    @Html.ActionLink("Create New", "Create")
</p>
<table class="table">
    <tr>
        <th>
            Текст задания
        </th>
        <th>
            Дата выполнения
        </th>
        <th></th>
    </tr>

@foreach (var task in Model.Tasks)
{
    <tr>
        <td>
            @Html.DisplayFor(modelItem=>task.Value.TaskText)
        </td>
        <td>
            @Html.DisplayFor(modelItem => task.Value.TillDate)
        </td>
    </tr>
}

</table>
phuzi
  • 12,078
  • 3
  • 26
  • 50
Gleb
  • 1,412
  • 1
  • 23
  • 55
  • Not related but what is your `EnumDropDownListFor` for? That will never bind when you submit because your have a `foreach` loop (refer [this answer](http://stackoverflow.com/questions/30094047/html-table-to-ado-net-datatable/30094943#30094943)) –  Jun 27 '17 at 07:10
  • You need to show your partial and the element its being loaded into and how your posting back the data. –  Jun 27 '17 at 07:11
  • @StephenMuecke I'v added UPD. I want to display validation message in main view, not in the pratial. – Gleb Jun 27 '17 at 07:21
  • @StephenMuecke I'm using ajax to change state of task when dropdown selection changed. – Gleb Jun 27 '17 at 07:22
  • Your adding the error to the partial view's `ModelState` - that will not add error to the main view. –  Jun 27 '17 at 07:33
  • I think you need to change your structure a bit. See https://stackoverflow.com/questions/18633923/asp-net-mvc-validation-in-partial-view-and-return-to-parent-view – Chris Halcrow Jun 27 '17 at 08:06
  • @StephenMuecke and how I can add error to MainView model state – Gleb Jun 27 '17 at 08:39
  • You cannot (unless you returned the main view). But there are lots of ways you could handle this (but unless you add the details noted in my 2nd comment, its impossible to tell what is the best way) –  Jun 27 '17 at 08:41
  • @StephenMuecke I'v added my partial view. Model class for this view is check list. – Gleb Jun 27 '17 at 09:07
  • Since all you displaying is 2 properties, I would forget the obsolete `Ajax` methods and use `$.ajax` and return just a `JsonResult` containing an value indicating success or failure- you already know those 2 values in the view so its just extra overhead to be returning the whole collection of tasks again when you could add the task using javascript/jquery. –  Jun 27 '17 at 09:11
  • And you can remove/disable the links once you have added the task, minimizing the risk of adding a task that has already been added (I assume multiple users might be adding tasks?). I am about to eat but can add an answer in about 45 min. –  Jun 27 '17 at 09:14
  • @StephenMuecke that's will be greate. But I'm passing whole objects becouse I also want to delete them from check list. – Gleb Jun 27 '17 at 10:04
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/147703/discussion-between-stephen-muecke-and-gleb). –  Jun 27 '17 at 10:12

2 Answers2

1

You cannot display a ModelState error from a partial in the main view's @Html.ValidationMessageFor() - that is razor code and is parsed on the server before its sent to the view, so will display ModelState errors from the main views model only.

One option would include moving the ValidationMessageFor() to the partial view with the only drawback being the ability to position the element.

However, returning the whole table of added tasks is unnecessary when you already know all the values in the client to append the new row to the table.

Change your code to use the $.ajax methods and in the method return a JsonResult indicating success or otherwise

Html

<a href="#" class="add" data-id="@item.Id">Add To check list</a>

Script

var url = '@Url.Action("AddToCheckList")';
var table = $('#CheckListPartial table);

$('.add').click(function() {
    var id = $(this).data('id');
    var cells = $(this).closest('tr').find('td');
    var text = cells.eq(0).text();
    var date = cells.eq(1).text();
    $.post(url, { id: id }, function(result) {
        if (result) {
            // create and append a new row
            var row = $('<tr></tr>');
            row.append($(<td></td>).text(text));
            row.append($(<td></td>).text(date));
            table.append(row);
        } else {
            // display your error
        }
    }).fail(function (result) {
        // display your error
    });
})

and the controller method would be

public JsonResult AddToCheckList(int id)
{
    if(context.CheckList.Tasks.Exists(t => t.Id == id))
    {
        return Json(null); indicate error - show a hidden element containing a message
    }
    context.AddTaskToCheckList(id);
    return Json(true); // indicate success
}

You should also consider removing the link if successful, so the user does not accidentally click it again.

A third alternative would be to generate a checklistbox of all tasks and allow the user to select or unselect items and the post a form and save all taks inone action. Refer this answer for an example of the approach.

0

You can make your Tasks to be an HashSet and check in the AddTaskToCheckList method if it is already there. In that case checking is O(1)

public void AddTaskToCheckList(int id)
{
    // Find task
    var task = this.Tasks.Find(id);
    if(!this.CheckList.Contains(task))
    {
        this.CheckList.Add(task);
    } 
    else
    {
        // Show error message        
    }   
}
Ivan Mladenov
  • 1,787
  • 12
  • 16