4

This is my first experience with lists in general and I have kinda complex scenario. I want to add an object of type Story that has a list of Sentences that can be added dynamically. Sentence has a one-to-one relationship with Image and another one-to-one relationship with Audio (that are optional to add). I managed to add the sentences list to the database along with the story object. But I have no idea where to start with the other two entities.

Here are the models for each entity:

public class Story
{
    public int StoryId { get; set; }
    public string Title { get; set; }
    public string Type { get; set; }
    public DateTime Date { get; set; }

    public virtual IEnumerable<Sentence> Sentences { get; set; } // one to many Story-Sentence
}

Sentence class:

public class Sentence
{
    public int Id { get; set; }
    public string SentenceText { get; set; }

    public virtual Audio Audio { get; set; } // one to one Sentence-Audio
    public virtual Image Image { get; set; } // one to one Sentence-Image
}

Image class:

public class Image
{
    [Key]
    [ForeignKey("Sentence")]
    public int Id { get; set; }
    public string ImageSelected { get; set; }
    public virtual Sentence Sentence { get; set; }
}

And the Audio class is exactly like the Image class. The view.

@model Story
<div id="editorRows">
    @foreach (var item in Model.Sentences)
    {
        <partial name="_SentenceEditor" model="item" />
    }
</div>
<a id="addItem" asp-action="BlankSentence" asp-controller="StoryTest">Add Sentence...</a> <br />
<input type="submit" value="Finished" />

The partial view

@model Sentence
<div class="editorRow">
    @using (Html.BeginCollectionItem("sentences"))
    {
        <span>Name: </span> @Html.EditorFor(m => m.SentenceText);
    }
    @using (Html.BeginCollectionItem("sentences"))
    {
        <span>Image: </span> @Html.EditorFor(m => m.Image.ImageSelected);
    }
    <a href="#" class="deleteRow">delete</a>
</div>

and I have some javascript that add and remove rows dynamically.

Finally in the Controller I'm just saving the model in the database

        [HttpPost]
    public async Task<IActionResult> AddTwo(Story model/*IEnumerable<Sentence> sentence*/)
    {
        if (ModelState.IsValid)
        {
            _db.Story.Add(model);
            await _db.SaveChangesAsync();
            return RedirectToAction("Index");
        }
        return View(model);
    }

In short, I want to add an Image and Audio along with the sentence. And also be able to access the entire row correctly for editing.

roo
  • 129
  • 1
  • 1
  • 14

3 Answers3

11

I made a demo based on your codes. You can store data in formData then use ajax pass it to the controller action.

Main View:

@model Story
<form asp-action="AddSentence" method="post">
    <div id="editorRows">
        <input type="hidden" name="" value="@Model.StoryId" />
        @foreach (var item in Model.Sentences)
        {
            <partial name="_SentenceEditor" model="item" />
        }
    </div>
    <a id="addItem" asp-action="BlankSentence" asp-controller="StoryTest">Add Sentence...</a>
    <br />
    <input type="submit" id="submit" value="Finished" />
</form>

@section scripts {
    <script>
        $("#submit").click(function (e) {
            e.preventDefault();
            var formData = new FormData();

            $("input[name='Audio.AudioSelected']").each(function (i) {
                var AudioSelected = $(this).val();
                formData.append("Sentences[" + i + "].Audio.AudioSelected", AudioSelected);

            });
            $("input[name='Image.ImageSelected']").each(function (i) {
                var ImageSelected = $(this).val();
                formData.append("Sentences[" + i + "].Image.ImageSelected", ImageSelected);

            });
            $("input[name='SentenceText']").each(function (i) {
                var SentenceText = $(this).val();
                formData.append("Sentences[" + i + "].SentenceText", SentenceText);

            });

            $.ajax({
                method: 'post',
                url: "StoryTest/AddSentence",
                data: formData,
                processData: false,
                contentType: false,
                success: function () {

                }
            });

        });

        $("#addItem").click(function () {
            $.ajax({
                url: this.href,
                cache: false,
                success: function (html) { $("#editorRows").append(html); }
            });
            return false;
        });

        $("a.deleteRow").on("click", function () {
            $(this).parents("div.editorRow:first").remove();
            return false;
        });
    </script>
}

Partial View:

@model Sentence
<div class="editorRow">

    <span>Name: </span> @Html.EditorFor(m => m.SentenceText)

    <span>Audio: </span> @Html.EditorFor(m => m.Audio.AudioSelected)

    <span>Image: </span> @Html.EditorFor(m => m.Image.ImageSelected)

    <a href="#" class="deleteRow">delete</a>
</div>

Controller:

public IActionResult Index()
{
    Story story = new Story
    {
        Sentences = new List<Sentence>
        {
            new Sentence { Id = 1, SentenceText = "AAA"
            , Audio = new Audio{ AudioSelected = "True"}
            , Image = new Image{ ImageSelected = "True"} },

            new Sentence { Id = 2, SentenceText = "BBB"
            , Audio = new Audio{ AudioSelected = "False"}
            , Image = new Image{ ImageSelected = "False"}},

            new Sentence { Id = 3, SentenceText = "CCC"
            , Audio = new Audio{ AudioSelected = "True"}
            , Image = new Image{ ImageSelected = "False"}}
        }
    };
    return View(story);
}

[HttpPost]
public async Task<IActionResult> AddSentence(Story model)
{
    if (ModelState.IsValid)
    {
        _db.Story.Add(model);
        await _db.SaveChangesAsync();
        return RedirectToAction("Index");
    }
    return View(model);
}

public IActionResult BlankSentence()
{
    return PartialView("_SentenceEditor", new Sentence());
}

Result:

enter image description here

mj1313
  • 7,930
  • 2
  • 12
  • 32
2

Option 3 - This is one I sometimes use but it does disconnect the business model from the database somewhat - but I include it for completeness.

You have one, simple record that contains the multiple types. You add just one extra fields that contains the data for the varying types. I make it a varchar(MAX) and name it something like JsonData.

What you then do is create a partial model based on the database class and add in all of the properties to get and set the type you want. These are simple one-liners.

You can also have more complex instances that might be a list of lists for example, which does not add complexity to the database context code.

Your view model and view don't really change compared to the database context class version. If you are using SQL Server you can also convert the JSON into columns that can be included in queries

/// <summary>
/// Gets or sets card instance from JSON data for resource. Null if not a card
/// </summary>
public Card CardGet
{
    get
    {
        if (this.ResourceTypeEnum == ResourceTypeEnumeration.Card)
        {
            return JsonSerializer.Deserialize<Card>(this.JsonData);
        }

        // not a card
        return null;
    }

    set
    {
        this.JsonData = JsonSerializer.Serialize(value);
    }
}
1

You have a few choices

Option 1 is to have one table for all child objects that has all fields for all types of object. Some might be common to all objects, e.g. Title, others will apply to one type of object. You need a field called something like ObjectTypeId - you can link this to a table that has a text name for each type. You can also create an enumeration for this ID to allow you to use C# switch and case statements to decide how to display objects, save values, and so on.

Option 2 is to have different objects in different tables. Your sentence then has multiple nullable fields that links to each type of object table, only one of which has an integer - the rest being null (unless you somehow want to have multiple objects in the same sentence). You can use an ObjectTypeId again or rely on one of the columns that link to children not being null. As in option 1, you might want to have an empty sentence for some reason - something to consider