Search code examples
asp.net-mvcasp.net-mvc-3razorienumerableforeach-loop-container

How can I bind a IEnumerable sequence to a container with multiple columns?


I believe that I couldn't find a proper title to explain my problem but I think this is the best possible short explanation.

Please let me explain the details.

I want to show a list of my pictures on a page and using a @foreach loop on MVC 3.

Partial View of this list as below:

@model IEnumerable<Picture>
@foreach (var item in Model)
{
    <a href="@item.PicId">
        <img height="35px" style="padding-top:3px" src="[email protected]" id="pictureMy" />
    </a>
}

As you may understand I am sending a list to this partialview and it is placing the pictures on a single column.

It is working without any problem but I want to show 3 pictures for each row but couldn't manage.

Any guidance will be very appreciated.

Thanks in advance for your helps.


Solution

  • You could group them by 3:

    @model IEnumerable<Picture>
    @foreach (var item in Model.Select((value, index) => new { value, index }).GroupBy(x => x.index / 3))
    {
        <div>
        @foreach (var picture in item)
        {
            <a href="@picture.value.PicId">
                <img height="35px" style="padding-top:3px" src="[email protected]" id="pictureMy" />
            </a>
        }
        </div>
    }
    

    But honestly this grouping is not something that should be done in the view. You should define a view model and then have your controller action perform the grouping and return the view model.

    So let's start by defining our view models:

    public class PictureViewModel
    {
        public int PicId { get; set; }
    }
    
    public class GroupedPicturesViewModel
    {
        public IEnumerable<PictureViewModel> Pictures { get; set; }
    }
    

    then the controller action:

    public ActionResult Index()
    {
        // fetch the pictures from the DAL or something
        IEnumerable<Picture> pictures = ... 
    
        // Now build the view model
        var model = pictures
            .Select((value, index) => new { value, index })
            .GroupBy(x => x.index / 3)
            .Select(x => new GroupedPicturesViewModel
            {
                Pictures = x.Select(p => new PictureViewModel
                {
                    PicId = p.value.PicId
                })
            }
        );
        return View(model);
    }
    

    then the corresponding view:

    @model IEnumerable<GroupedPicturesViewModel>
    @Html.DisplayForModel()
    

    then the corresponding display template for the GroupedPicturesViewModel type (~/Views/Shared/DisplayTemplates/GroupedPicturesViewModel.cshtml):

    @model GroupedPicturesViewModel
    <div>
        @Html.DisplayFor(x => x.Pictures)
    </div>
    

    and finally the display template for the PictureViewModel type (~/Views/Shared/DisplayTemplates/PictureViewModel.cshtml):

    @model PictureViewModel
    <a href="@Model.PicId">
        <img class="image" src="@Url.Content("~/ImageHandler.ashx?id=" + Model.PicId)" alt="" />
    </a>
    

    One final thing that's bugging me is this anchor. Looks ugly. Don't you think? Looks like spaghetti code.

    Let's improve it by writing a custom, reusable HTML helper which will render those pictures:

    public static class HtmlExtensions
    {
        public static IHtmlString Picture(this HtmlHelper<PictureViewModel> htmlHelper)
        {
            var anchor = new TagBuilder("a");
            var picture = htmlHelper.ViewData.Model;
            var id = picture.PicId.ToString();
            var urlHelper = new UrlHelper(htmlHelper.ViewContext.RequestContext);
    
            // You probably need another property on your view model here as this 
            // id is suspicious about href but since that's what you had in your 
            // original code I don't know what is your intent.
            anchor.Attributes["href"] = id;
            var image = new TagBuilder("img");
            image.Attributes["alt"] = "";
            image.Attributes["src"] = urlHelper.Content(
                "~/ImageHandler.ashx?id=" + urlHelper.Encode(id)
            );
            image.AddCssClass("image");
            anchor.InnerHtml = image.ToString();
            return new HtmlString(anchor.ToString());
        }
    }
    

    and then in the display template we will simply have:

    @model PictureViewModel
    @Html.Picture()
    

    And that's pretty much it. No need to write loops. Everything works by convention.