Элегантная конструкция foreach — else в Razor

#.net #asp.net-mvc #razor #templating #twig

#.net #asp.net-mvc #razor #создание шаблонов #веточка

Вопрос:

Многие механизмы создания шаблонов имеют особый синтаксис, представляющий собой комбинацию foreach и else . В основном else предложение выполняется, когда в foreach цикле нет итераций. Это может быть полезно, если вы хотите отобразить какой-то резервный вариант отсутствия элементов в списке.

Например, в Twig for цикл может выглядеть так

 {% for user in users %}
    <li>{{ user.username|e }}</li>
{% else %}
    <li><em>no user found</em></li>
{% endfor %}
  

Используя механизм просмотра Razor, шаблон должен выглядеть следующим образом, включая дополнительную проверку количества элементов в коллекции:

 @foreach (var user in users) {
    <li>@user.UserName</li>
}
@if (!users.Any()) {
    <li><em>no user found</em></li>
}
  

Итак, мои вопросы: можем ли мы достичь подобной элегантности тем или иным способом, используя механизм просмотра Razor.

Ответ №1:

Консолидация ответов Джейми и Мартина Бута. Я создал следующий метод расширения. Он принимает IEnumerable в качестве первого аргумента, а затем два делегата для отображения текста. В представлениях Razor мы можем передать шаблонным делегатам два этих параметра. Короче говоря, это означает, что вы можете вводить шаблоны. Итак, вот метод расширения и как вы можете его вызвать:

     public static HelperResult Each<TItem>(this IEnumerable<TItem> items, 
        Func<TItem, HelperResult> eachTemplate, 
        Func<dynamic, HelperResult> other)
    {
        return new HelperResult(writer =>
        {
            foreach (var item in items)
            {
                var result = eachTemplate(item);
                result.WriteTo(writer);
            }

            if (!items.Any())
            {
                var otherResult = other(new ExpandoObject());
                // var otherResult = other(default(TItem));
                otherResult.WriteTo(writer);
            }
        });
    }
  

И в представлениях Razor:

 @Model.Users.Each(
    @<li>@item.Name</li>,
    @<li>
        <b>No Items</b>
     </li>
)
  

В целом, довольно чисто.

ОБНОВИТЕ реализацию предложений, сделанных в комментариях. Этот метод расширения принимает один аргумент для перебора элементов в коллекции и возвращает пользовательский HelperResult . В этом helperresult можно вызвать Else метод для передачи делегата шаблона в случае, если элементы не найдены.

 public static class HtmlHelpers
{
    public static ElseHelperResult<TItem> Each<TItem>(this IEnumerable<TItem> items, 
        Func<TItem, HelperResult> eachTemplate)
    {
        return ElseHelperResult<TItem>.Create(items, eachTemplate);
    }
}

public class ElseHelperResult<T> : HelperResult
{
    private class Data
    {
        public IEnumerable<T> Items { get; set; }
        public Func<T, HelperResult> EachTemplate { get; set; }
        public Func<dynamic, HelperResult> ElseTemplate { get; set; }

        public Data(IEnumerable<T> items, Func<T, HelperResult> eachTemplate)
        {
            Items = items;
            EachTemplate = eachTemplate;
        }

        public void Render(TextWriter writer)
        {
            foreach (var item in Items)
            {
                var result = EachTemplate(item);
                result.WriteTo(writer);
            }

            if (!Items.Any() amp;amp; ElseTemplate != null)
            {
                var otherResult = ElseTemplate(new ExpandoObject());
                // var otherResult = other(default(TItem));
                otherResult.WriteTo(writer);
            }
        }
    }

    public ElseHelperResult<T> Else(Func<dynamic, HelperResult> elseTemplate)
    {
        RenderingData.ElseTemplate = elseTemplate;
        return this;
    }

    public static ElseHelperResult<T> Create(IEnumerable<T> items, Func<T, HelperResult> eachTemplate)
    {
        var data = new Data(items, eachTemplate);
        return new ElseHelperResult<T>(data);
    }

    private ElseHelperResult(Data data)
        : base(data.Render)
    {
        RenderingData = data;
    }

    private Data RenderingData { get; set; }
}
  

Затем это можно вызвать следующим образом:

 @(Model.Users
   .Each(@<li>@item.Name</li>)
   .Else(
        @<li>
            <b>No Users</b>
         </li>
        )
)
  

Комментарии:

1. Запятая почти невидима. 😛

2. Я согласен, но нет никакого реального способа обойти это, не так ли? 🙂

3. Если вы используете именованный параметр для предложения else, он, вероятно, будет еще более читаемым, и вы можете сделать его необязательным

4. Я думаю, вы могли бы получить либо из HelperResult , либо из IHtmlString и создать .Else() метод аналогичным образом. Итак, вы бы написали: @Models.Users.Each(@<li>@item.name</li>).Else(@<li><b>No items</b></li>)

5. Еще лучше, верните пользовательский класс из метода for each extention, который реализует ihtmlstring, но это означает, что вы можете заставить метод else применяться только к этому пользовательскому классу (т.Е. Не к любому ienumerable)

Ответ №2:

Единственный способ, которым я мог подумать, добиться чего-то подобного, — это использовать пару расширений для IEnumerable<T> :

 public static class IEnumerableExtensions
{
    public static IEnumerable<T> ForEach<T>(this IEnumerable<T> enumerable, Action<T> action)
    {
       foreach(T item in enumerable)
           action(item);

        return enumerable;
    }

    public static IEnumerable<T> WhenEmpty<T>(this IEnumerable<T> enumerable, Action action)
    {
       if(!enumerable.Any())
           action();
        return enumerable;
    }
}
  

Это позволяет вам связывать 2 вызова друг с другом, как показано в этом живом примере: http://rextester.com/runcode?code=AEBQ75190 который использует следующий код:

 var listWithItems = new int[] {1,2,3};
var emptyList = new int[]{};

listWithItems.ForEach(i => Console.WriteLine(i))
    .WhenEmpty( () => Console.WriteLine("This should never display"));

emptyList.ForEach(i => Console.WriteLine(i))
    .WhenEmpty( () => Console.WriteLine("This list was empty"));
  

Как это будет вписываться в шаблон Razor, в котором я все еще не уверен …. но, возможно, это даст вам что-то для продолжения.

Ответ №3:

В afaik ничего не построено, но вы, вероятно, могли бы расширить это в соответствии с вашими потребностями:

http://haacked.com/archive/2011/04/14/a-better-razor-foreach-loop.aspx

Возможно, я смогу помочь позже, когда не буду пользоваться своим телефоном, если у вас все еще нет ответа

Комментарии:

1. Я думаю, что если вы объедините это с ответом Джейми, у вас будет решение!

Ответ №4:

Возможно, это было невозможно, когда был задан вопрос, но я только что добился этого следующим образом:

 @if (Model.EmailAddress.Count() > 0)
{
    foreach (var emailAddress in Model.EmailAddress)
    {
        <div>@emailAddress.EmailAddress</div>
    }
} else { <span>No email addresses to display</span>  }