Строготипизированные URL в Javascript c T4MVC

Если кто не знает, то T4MVC — это замечательная штука, которая позволяет строготипизировать в MVC3 то, что еще недостроготипизировано из коробки :)
В частности, с его помощью очень удобно генерировать ссылки на MVC-экшены в хтмл:

// в контроллере:
public ActionResult Index(int a, string b) {} 

// во вьюшке (Razor)
<a href="@Url.Action(MVC.Home.Index(10, 'some_string'))">link</a>  

//в браузере на клиенте
<a href="/Home/Index?a=10&b=some_string">link</a>

Проблемы возникают, когда параметры экшена — динамические, и их значения можно определить только в рантайме (например, параметром является какой-нибудь javascript-атрибут).

Допустим, у нас есть список, и по клику на любом его элементе в нижележащий div загружается страничка с сервера (по ссылке вида: http://localhost/Home/Region?id={element_id}), причем эта ссылка, как видно, зависит от id элемента в списке.
Пример в псевдокоде:

<ul>
  <li id="153" onclick="HandleClick();">Первый элемент</li>
  <li id="215" onclick="HandleClick();">Второй элемент</li>
</ul>

<script>
function HandleClick() {
    var id = $(this).attr("id");
    $('#somediv').load("http://localhost/Home/Region?id="+id);  
}
</script>

В примере выше все работает, но ссылка забита прямо в код, и если у нас изменится имя экшена, или имя параметра, или добавится еще один параметр в экшен, то мы об этом узнаем, только когда у кого-нибудь из клиентов что-то «свалится» :) Поэтому привлечем T4MVC и сделаем ссылку строготипизированной, тогда все ошибки мы будем иметь на стадии компиляции.
Очень хочется сделать так:

    var id = $(this).attr("id");
    $('#somediv').load("@Url.Action(MVC.Home.Region(id))");  

но, естественно, это невозможно, потому что id — это javascript-переменная, а @Url.Action выполняется на сервере.

Неплохим решением будет использовать workaround типа:

    var id = $(this).attr("id");
    $('#somediv').load("@Url.Action(MVC.Home.Region(-1))".replace('-1', id));  

В этом случае в яваскрипте у нас будет урл вида: «http://localhost/Home/Region?id=-1», и на уровне javascript мы заменим «-1» на требуемый нам id.
Данный прием неплох, но его минус в том, что подставляемое значение (-1 в нашем случае) должно совпадать с типом переменной, и в случае когда параметр, например, типа Guid задать значение «по умолчанию» не так просто.

Для таких случаев я написал небольшой класс-helper, который помогает получать url очень похожие на шаблоны String.Format: «http://localhost/Home/Region?id=(0)&param=(1)»

    //пример использования:
    @Url.ActionWithPlaceholders(MVC.Home.Region(Placeholders.Guid("(0)"), Placeholders.Guid("(1)"))

    //на выходе будет
    http://localhost/Home/Region?guid1=(0)&guid2=(1)

Суть работы в генерации псевдо-значений нужных типов (функция Placeholders.Guid), подстановке их в URL, и дальнейшей замене псевдо-значений на плейсхолдеры (Url.ActionWithPlaceholders)

P.S. Исследуя проблему, я задавал этот вопрос на stackoverflow. Любопытствующие по ссылке смогут прочитать мнение Дэвида Эббо на описанную проблему.

P.P.S.
Полный код класса для ознакомления с деталями реализации и/или копипасты (применительно к параметрам типа Guid) :)

    // аналог функции @Url.Action, включающий в себя процессинг плейсхолдеров
    public static string ActionWithPlaceholders(this UrlHelper urlHelper, ActionResult result)
    {
        var routeValues = result.GetRouteValueDictionary();
        var newRouteValues = new RouteValueDictionary();
        foreach (var routeValue in routeValues)
        {
            newRouteValues[routeValue.Key] = Placeholders.GetPlaceholderFor(routeValue.Value);
        }

        Placeholders.Clear();

        return urlHelper.RouteUrl(null, newRouteValues);
    }


    // класс для хранения плейсхолдеров и псевдо-значений
    public class Placeholders
    {
        private static readonly Dictionary<Guid, string> GuidPlaceholders = new Dictionary<Guid, string>();
        public static Guid Guid(string placeholder)
        {
            var value = System.Guid.NewGuid();
            while (GuidPlaceholders.ContainsKey(value))
                value = System.Guid.NewGuid();

            GuidPlaceholders[value] = placeholder;
            return value;
        }

        public static object GetPlaceholderFor(object originalValue)
        {
            var valueType = originalValue.GetType();
            if (typeof(Guid) == valueType)
            {
                var typedValue = (Guid)originalValue;
                if (GuidPlaceholders.ContainsKey(typedValue))
                {
                    return GuidPlaceholders[typedValue];
                }
            }

            return originalValue;
        }

        public static void Clear()
        {
            IntPlaceholders.Clear();
            GuidPlaceholders.Clear();
        }
    }
Опубликовать в Facebook
Опубликовать в Google Plus

2 комментария

  1. Неплохо, хотя и костыль.

    Думаю, скобочки из Placeholders.Guid(«(0)») можно убрать

  2. Костылек, согласен. Но проблему решил, а более красивых решений не придумалось :)
    Скобочки да, можно убрать.. а можно и оставить, будет чуть понятнее при первом взгляде :)

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *