0

I am trying to create a asp.net mvc site using c#. I have two models an Event model and a Ticket model. I want to be able to create an Event using the supplied CRUD views and then when create is clicked the user should be forwarded to another page where they can add tickets to this event. So far I have got it working so an event can be created and the user can create one ticket. However I need multiple tickets per event so I believe a ViewModel may be useful but I've no idea what it would look like or how to use it to allow multiple tickets to be added in my view. Here are my current models;

public class Event
{
    public int EventID { get; set; }

    [Required]
    public String Name { get; set; }

    [Required]
    public String Location { get; set; }

    [Required]
    [DataType(DataType.Date)]
    public DateTime Date { get; set; }

    [Required]
    [DataType(DataType.Time)]
    public DateTime StartTime { get; set; }

    [Required]
    [DataType(DataType.MultilineText)]
    public String Description { get; set; }

    [Required]
    public int TicketsAvailable { get; set; }

    //navigation property
    public virtual ICollection<Order> Order { get; set; }
    //navigation property
    public virtual ICollection<Ticket> Ticket { get; set; }
}    

public class Ticket
{
    public int TicketID { get; set; }

    [Required]
    [ForeignKey("Event")]
    //foreign key
    public int EventID { get; set; }

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

    [Required]
    public float Price { get; set; }

    //navigation property
    public virtual Event Event { get; set; }

    //navigation property
    public ICollection<OrderDetails> OrderDetails { get; set; }
}

Here is my attempt at an appropriate viewmodel

public class EventTicketViewModel
{
    public Event events {get; set;}
    public List<Ticket> tickets { get; set; }
}    

Here is my create Ticket view

@model GeogSocSite.Models.Ticket

@{
ViewBag.Title = "Create";
}

<h2>Create</h2>

@using (Html.BeginForm()) 
{
@Html.AntiForgeryToken()

<div class="form-horizontal">
    <h4>Ticket</h4>
    <hr />
    @Html.HiddenFor(model => model.EventID)

    <div class="form-group">
        @Html.LabelFor(model => model.Description, htmlAttributes: new { @class = "control-label col-md-2" })
        <div class="col-md-10">
            @Html.EditorFor(model => model.Description, new { htmlAttributes = new { @class = "form-control" } })
            @Html.ValidationMessageFor(model => model.Description, "", new { @class = "text-danger" })
        </div>
    </div>

    <div class="form-group">
        @Html.LabelFor(model => model.Price, htmlAttributes: new { @class = "control-label col-md-2" })
        <div class="col-md-10">
            @Html.EditorFor(model => model.Price, new { htmlAttributes = new { @class = "form-control" } })
            @Html.ValidationMessageFor(model => model.Price, "", new { @class = "text-danger" })
        </div>
    </div>

    <div class="form-group">
        <div class="col-md-offset-2 col-md-10">
            <input type="submit" value="Create" class="btn btn-default" />
        </div>
    </div>
</div>

}

<div>
@Html.ActionLink("Back to List", "Index")
</div>

And finally here is my TicketController class

public class Tickets1Controller : Controller
{
    private ApplicationDbContext db = new ApplicationDbContext();
    private TicketRepository _ticketRepository;

    public Tickets1Controller()
    {
        this._ticketRepository = new TicketRepository();
    }

    // GET: Tickets
    [AllowAnonymous]
    public ActionResult Index()
    {
        return View(_ticketRepository.GetTickets());   
    }

    // GET: Tickets1/Details/5
    [AllowAnonymous]
    public ActionResult Details(int? id)
    {
        if (id == null)
        {
            return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
        }
        Ticket ticket = _ticketRepository.GetByID(id);
        if (ticket == null)
        {
            return HttpNotFound();
        }
        return View(ticket);
    }

    // GET: Tickets1/Create
    [AllowAnonymous]
    public ActionResult Create(int id)
    {
        Ticket ticket = new Ticket();
        ticket.EventID = id;
        return View(ticket);
    }

    // POST: Tickets1/Create
    [HttpPost]
    [ValidateAntiForgeryToken]
    [AllowAnonymous]
    public ActionResult Create([Bind(Include = "TicketID,EventID,Description,Price")] Ticket ticket)
    {
        if (ModelState.IsValid)
        {
            _ticketRepository.InsertTicket(ticket);
            _ticketRepository.Save();
            return RedirectToAction("Index");
        }
        return View(ticket);
    }

    // GET: Tickets1/Edit/5
    [AllowAnonymous]
    public ActionResult Edit(int? id)
    {
        if (id == null)
        {
            return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
        }
        Ticket ticket = _ticketRepository.GetByID(id);
        if (ticket == null)
        {
            return HttpNotFound();
        }
        return View(ticket);
    }

    // POST: Tickets1/Edit/5
    [HttpPost]
    [ValidateAntiForgeryToken]
    [AllowAnonymous]
    public ActionResult Edit([Bind(Include = "TicketID,EventID,Description,Price")] Ticket ticket)
    {
        if (ModelState.IsValid)
        {
            _ticketRepository.UpdateTicket(ticket);
            return RedirectToAction("Index");
        }

        return View(ticket);
    }

    // GET: Tickets1/Delete/5
    [AllowAnonymous]
    public ActionResult Delete(int? id)
    {
        if (id == null)
        {
            return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
        }
        Ticket ticket = _ticketRepository.GetByID(id);
        if (ticket == null)
        {
            return HttpNotFound();
        }
        return View(ticket);
    }

    // POST: Tickets1/Delete/5
    [HttpPost, ActionName("Delete")]
    [AllowAnonymous]
    [ValidateAntiForgeryToken]
    public ActionResult DeleteConfirmed(int id)
    {
        _ticketRepository.DeleteTicket(id);
        _ticketRepository.Save();
        return RedirectToAction("Index");
    }

    protected override void Dispose(bool disposing)
    {
        if (disposing)
        {
            db.Dispose();
        }
        base.Dispose(disposing);
    }
}

If anyone can help me with how to either create the event and add multiple tickets to the event all on one page or how to save the event then create multiple tickets I'd be really grateful as I've been stuck on this for ages now.

Sergii Zhevzhyk
  • 4,074
  • 22
  • 28
Myles57
  • 3
  • 1
  • If your wanting to dynamically add `Tickets` for and `Event` then suggest you look at the answers [here](http://stackoverflow.com/questions/29161481/post-a-form-array-without-successful/29161796#29161796) and [here](http://stackoverflow.com/questions/28019793/submit-same-partial-view-called-multiple-times-data-to-controller/28081308#28081308) and [here](http://stackoverflow.com/questions/29837547/set-class-validation-for-dynamic-textbox-in-a-table/29838689#29838689). And you can create the even and its tickets in one form and post it all back at once –  Dec 03 '15 at 22:33

1 Answers1

0

Since we have to dynamically add a row for a new ticket, We should be using some javascript to handle the form building and posting.

Create a viewmodel for the Ticket creation page

public class CreateTicketVm
{
    public int EventId { get; set; }
    public List<TicketVm> Tickets { set; get; }
}

public class TicketVm
{
    [Required]
    public string Description { get; set; }

    [Required]
    public float Price { get; set; }
}

And in the GET action, We will create an object of CreateTicketVm, Set the Event property value and send it to the view.

public ActionResult CreateTicket(int id)
{
    var vm = new CreateTicketVm {EventId = id};       
    return View(vm);
}

And in your view

@model YourNamespace.CreateTicketVm
<div id="tickets"></div>
@Html.HiddenFor(s => s.EventId)

<input type="button" id="add" value="Add Ticket" />
<input type="submit" id="btnSave" />

Now we need to add some javascript to our allow user to create a new ticket row when they click on "Add Ticket" button. Also we will listen to the click event on the submit button and we will build a javascript object which looks same as our CreateTicketVm viewmodel. and we will post (using ajax) the json version of this object to the MVC controller. Once the ticket information is saved, The action method will return a json response which indicates whether the operation was successful. In the done event of the ajax post, we will inspect this response and redirect the usr to the next page(or do some custom thing as needed).

<script>
    $(function () {
       //Lets create the first row
        addNewRow();

        $("#add").click(function (e) {
            e.preventDefault();
            addNewRow();    
        });

        $("#btnSave").click(function (e) {
            e.preventDefault();

            var createTicket = { EventId: $("#EventId").val(), Tickets: [] };
            var rows = $("#tickets").find(".item");

            $.each(rows, function (i, row) {
                var _row = $(this);

                var inputs = _row.find("input");
                var ticket = {};
                $.each(inputs, function (index, item) {
                    var _this = $(this);
                    ticket[_this.data("for")] = _this.val();
                });
                createTicket.Tickets.push(ticket);
            });

            var targetUrl = "@Url.Action("CreateTicket","Home")";
            $.ajax({
                url: targetUrl,
                contentType: "application/json",
                data: JSON.stringify(createTicket),
                type: "POST"
            }).done(function(res) {
                if (res.Status === "Success") {
                    window.location.href = "@Url.Action("Success")";
                }
            });
        });
    });

    function addNewRow() {          
        $.get("@Url.Action("AddNewTicketRow","Home")", function (res) {    
            $("#tickets").append(res);    
        });    
    }
</script>

You can see that the addNewRow method is also making an ajax call to get the markup for each ticket row. So let's add an action method for that.

public ActionResult AddNewTicketRow()
{
    var vm = new TicketVm ();
    return PartialView("~/Views/Home/Partials/AddNewTicketRow.cshtml", vm);
}

You can see that we are returning a partial view. So make sure to create the partial view in that location with the below content.

@model TicketVm

<div class="item">
    @Html.TextBoxFor(s => s.Description, new {@data_for = "Description"}) 
    Price : @Html.TextBoxFor(s=>s.Price, new {@data_for="Price"})
</div>

We can build the markup directly in js. But it gets ugly when your markup has more items. So creating a partial view is safe and helps us to easily make markup changes as needed in future.

When user clicks the submit, we are posing the Json version of the object to the CreateTicket HttpPost action method. So let's create that too.

[HttpPost]
public ActionResult CreateTicket([FromBody] CreateTicketVm model)
{
    if (model != null)
    {
       //check model.Tickets and model.EventId
       // to do : Save Tickets for the event
        return new JsonResult( new { Status ="Success"});
    }
    return new JsonResult( new { Status ="Error"});
}

The above code is is for Asp.Net MVC 6. In MVC 6, Mvc controllers and Web Api controllers are not separate. They both inherit from Controller class which is in Microsoft.AspNet.Mvc namespace.

If you are using a version prior to MVC 6, You do not need the [FromBody] attribute in your HttpPost action method. Also, for returning json data, you can use Json method.

[HttpPost]
public ActionResult CreateTicket(CreateTicketVm model)
{
    if (model != null)
    {
        //check model.Tickets and model.EventId
        // to do : Save Tickets for the event
        return Json(new {Status = "Success"});
    }
    return Json(new {Status = "Error"});
}
Shyju
  • 214,206
  • 104
  • 411
  • 497
  • Thank you for your detailed answer it is extremely helpful. I have just a few issues when implementing this, for the CreateTicket Post method I'm getting errors (saying the type or namespace name FormBodyAttribute could not be found, are you missing a using directive or an assembly reference?) I've googled this error but haven't found a fix. Sorry if I've misunderstood the method! – Myles57 Dec 03 '15 at 21:11
  • Remove the `[FromBody]` - that's only for mvc web-api –  Dec 03 '15 at 22:07
  • Do I need to replace it with anything else @StephenMuecke I'm a bit confused about this whole method as there are errors related to the return new Jon result code as well – Myles57 Dec 03 '15 at 22:26
  • If you are using MVC6, you need [FromBody] attribute as there is no seperate Mvc or Web api controllers. For MVC5 or earlier versions, you can ignore that attribute. You should also use `Json` method instead of `JsonResult` . See my updated answer. – Shyju Dec 03 '15 at 23:09
  • @Shyju thanks for the update for MVC5. I'm having a problem with the partial view rendering. When the Add Ticket button is clicked nothing happens. I've made sure to match file names and created a Partials folder and placed my AddNewTicketRow view in here. – Myles57 Dec 04 '15 at 10:06
  • @Myles57 Are you getting any script errors in console ? 404 or 500 error in network tab ? It is is a 404 , It could be a bad url in the $.get method since i hard coded the url. I just changed my answer to use `Url.Action` method to generate the url. Try that and see what happens. Use browsers developer tool->Network tab to see what ajax request the code is sending and what response is coming back – Shyju Dec 04 '15 at 12:55
  • @Shyju I've replaced the code with the updated code but it's still giving a 404 error, I went to the developer tools and it says Uncaught ReferenceError: $ is not defined in regards to the first line in the script. There is also an error GET http://localhost:14192/Content/jqueryui . I don't understand this because I have checked my package manager console and i've got jqueryui installed. – Myles57 Dec 04 '15 at 14:22
  • @Myles57 You should put your page scripts inside `@section scripts` .See here https://gist.github.com/kshyju/171599ec5e6f6114b130 . Also make sure that you have jQuery loaded properly in your page(I guess that will probably be in the Layout file alread) the 404 jqueryui error has nothing to do with our changes and our code is not dependent on that libarary. But you might want to fix that. do you have jquery ui library in Content folder, if not, use the correct path – Shyju Dec 04 '15 at 14:33
  • @Shyju adding the scripts section has worked thank you because a new row can now be added when the Add Ticket button is clicked. However when submit is clicked nothing happens and I've had a look at the developer tools to see the errors but it just says failed to load resource regarding the UI again, but I checked and jqueryui is in the Content folder. – Myles57 Dec 04 '15 at 15:11
  • network tab should show you an http call on the submit event. it should hit the action method. If you copy and paste the code i provided, it should work fine (I verified it). So i guess you will have some other problem in your page which is causing the issue. To find out what it is, just create a new page and try the code i provided, nothing else (of course you need to load jQuery library in your layout) – Shyju Dec 04 '15 at 15:14
  • @Shyju I've looked in the network tab and when submit is clicked the status remains pending. I'll create a new page and try it if I can get it working. – Myles57 Dec 04 '15 at 15:28