kolin kolin - 3 months ago 17
ASP.NET (C#) Question

mapping images to pages within asp.net mvc

Problem
I need to map one model within the edit view of another. I have an image and within it's edit view I have two tabs (the image edit tab and the mapped pages tab).
The 'edit view' tab works as expected. and the 'mapped pages' tab correctly lists all the pages that are mapped to the image.
What I would like to do is to have a form within the 'mapped pages' tab where I can select a page to map this image to. I have tried encompassing the whole edit view within a form and doing an ActionResult based upon the submit value (didn't work) and I have tried creating the form as a partial view, but that didn't work either. Rather than hack it, I would like to try and do best practice. I have read about Editor Templates but I am not sure they are what I need.

I have the following three models (narrowed down for the scope of this question):

Page

public class PageContent
{
public int PageId {get;set;}
public string PageTitle {get;set;}
public string PageContent {get;set;}

public virtual ICollection<PageImageXmap> PageImages {get;set;}
}


Image

public class SiteImage
{
public int ImageId {get;set;}
public string ImageTitle {get;set;}
public string ImageFilename {get;set;}

public virtual ICollection<PageImageXmap> ImagePages {get;set;}
}


Xmap

public class PageImageXmap
{
[Key, Column(Order = 0)]
public int PageId {get;set;}
[Key, Column(Order = 1)]
public int ImageId {get;set;}

public virtual PageContent PageContents {get;set;}
public virtual SiteImages SiteImages {get;set;}
}


The beginning of the controller method that got me the most success (it actually added the PageId and ImageId to the Xmap table/model - but fell down when trying to reload the page) is Updated:

public ActionResult Edit(Int? id)
{
/*get image from DB*/
ViewBag.Pages = new SelectList(db.PageContents, "PageId", "PageTitle")
return View(siteImage);
}

public ActionResult Edit(SiteImage siteImage, string AddXMap, string Save, string PageId)
{
if(!String.IsNullOrEmpty(AddXMap) // If the map submit button is click
{
/* adds the xmap model to the database */
ViewBag.Pages = new SelectList(db.PageContents, "PageId", "PageTitle")
return View(siteImage);
}
else
{
/* saves the existing siteImage */
return View(siteImage);
}
}


This fell over (500: Object reference not set to instance of an object on PageTitle) on returning the actual -Edit View- though when I list the Pages that this image is mapped to. If I hard reload the page it all works. Updated where error occurs

@model MyCode.DAL.SiteImage

/* Razor code for editing the image details */

@foreach(var item in Model.ImagePagesXMap) // <--- Object reference not set to an instance of an object on return View
{
<tr>
<td>@item.PageContents.PageTitle</td>
</tr>
}
@Html.DropDownList("PageId", (IEnumerable<SelectListItem>)Viewbag.Pages)
<input type="submit" name="AddXMap" id="AddXMap" value="Map current image to page" />


Update #1
show more controller/Razor code and where 500 occurs

Update #2
If I add
siteImage = db.SiteImages.Find(siteImage.ImageId);
before I
return view(siteImage);
then I get an
object reference not set
on
@item.PageContents.PageTitle
as opposed to Model.ImagePagesXMap. I tried this because this is the code that correctly works when the edit view is first loaded. As no image data is edited, i figured this would just reload the siteImage object, but nope.

Question
Am I missing something obvious here, is there a better way to do this? I've done a lot of searching and I can't find anybody trying anything similar which makes me think that I'm approaching this wrong. (I'm a bit of an MVC n00b)

Answer

So I actually managed to sort this myself. It doesn't seem the correct way to do this, but it works enough for the moment.

in my razor view I ended up with :

<h2>Image on pages</h2>
        <input id="PageIdToRemove" name="PageIdToRemove" value="0" type="hidden" />
        <input id="RemoveXMap" name="RemoveXMap" value="" type="hidden" />
        <table class="table table-striped">
            <tr>
                <th>Page title</th>
                <th>Image order</th>
                <th>Actions</th>
            </tr>
            @foreach (var item in Model.ImagePagesXMap) {
            <tr>

                <td>@item.PageContents.PageTitle</td>
                <td>@item.ImageOrder</td>
                <td><button class="btn  btn-danger  js-button-removexmap" data-buttonvalue="@item.PageContents.PageId">
                                <span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span>
                                Remove image from page</button></td>
            </tr>
            }
        </table>
        <h2>Add this image to another page</h2>
        <div class="form-group">
            <label for="PageId">Page</label>
            @Html.DropDownList("PageId", (IEnumerable<SelectListItem>)ViewBag.Pages, String.Empty, new { @class="form-control"})
        </div>
        <div class="form-group">
            <label for="ImageOrder">Order on page</label>
            <input type="number" id="ImageOrder" name="ImageOrder" class="form-control" />
        </div>
        <hr />
        <p><input type="submit" name="AddXMap" id="AddXMap" value="Add to page" class="btn btn-success" /> | @Html.ActionLink("Cancel", "Index", null, new { @class = "btn btn-default" })</p>
<script type="text/javascript">
$(function () {
            $('.js-button-removexmap').click(function () {
                $('#PageIdToRemove').val($(this).data('buttonvalue'));
                $('#RemoveXMap').val('1');
                $('form').submit();
            });
});
</script>

The majority of the code on here is to actually remove a crossmapped image. In this case it sets the value of #PageIdToRemove to be the item id specified on the data part of the button element, and also a value of RemoveXMap.

Within the Controller I have :

[HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult Edit(SiteImage siteimage, string AddXMap, string Save, int ImageOrder, int PageId, string RemoveXMap, int PageIdToRemove)
        {


            if (!String.IsNullOrEmpty(AddXMap))
            {
                if (PageId > 0)
                {
                    var xmapAdd = new MyLibrary.BLL.GetPageImageXMap();
                    xmapAdd.AddImageXMap(siteimage.ImageId, PageId, ImageOrder);

                    var imageQuery = new MyLibrary.BLL.GetImagesQuery();
                    siteimage = imageQuery.GetImage(siteimage.ImageId);
                }

            }

            if (!String.IsNullOrEmpty(RemoveXMap))
            {
                var xmapRemove = new MyLibrary.BLL.GetPageImageXMap();
                xmapRemove.RemoveImageXMap(siteimage.ImageId, PageIdToRemove);

                var imageQuery = new MyLibrary.BLL.GetImagesQuery();
                siteimage = imageQuery.GetImage(siteimage.ImageId);
            }

            if (!String.IsNullOrEmpty(Save))
            {
                if (ModelState.IsValid)
                {
                    db.Entry(siteimage).State = EntityState.Modified;
                    db.SaveChanges();
                }
            }

            ViewBag.Pages = new SelectList(db.PageContents, "PageId", "PageTitle");

            return View(siteimage);
        }

My thoughts were that the siteimage originally being returned was from the same context as posted, and therefore didn't require refreshing. so this bit of code:

var imageQuery = new MyLibrary.BLL.GetImagesQuery();
siteimage = imageQuery.GetImage(siteimage.ImageId);

actually reloads the siteimage with the newly saved xmap data. (because I haven't editted the siteimage, i've just used the imageid of it to add or delete an xmap)

and this view is then passed back to the view. Whether this is the best way to do this (probably not) is another question for another day. if anyone would like to suggest alternative solutions thats also cool.

Comments