Как WPF резолвит ссылки на ещё не созданные элементы

Written by elwood

xamllogo

Используя стандартную конструкцию {Binding ElementName=myElement}, никогда не задумывался о том, как происходит разрешение элемента по имени, если этот элемент определён дальше. А когда сам стал писать XAML-подобный механизм, озадачился. Ведь парсер должен строить граф объектов последовательно, сверху вниз, читая атрибуты и содержимое тегов и тут же их применяя к текущему конструируемому объекту. Но как в таком случае разруливать ссылки на элементы, до которых парсер ещё не добрался ? Неужели WPF обрабатывает расширения разметки отдельно, уже после построения графа объектов ? Это было бы весьма нелогично. Документация по этому вопросу отсутствовала, было только описание интерфейса IXamlNameResolver с сигнатурами методов, по которым можно догадаться о том, что метод Resolve может возвращать ссылки на не до конца инициализированные объекты, а заказывать нужные элементы можно через GetFixupToken.

Поискав на stackoverflow с полчаса, я нашёл замечательнейший ответ на свой вопрос.

Работает всё следующим образом. Классы, зарегистрированные в качестве расширений разметки, вызываются тотчас же для преобразования строки в объект. Метод ProvideValue может вернуть либо готовый объект, либо FixupToken, получаемый от сервиса IXamlNameResolver. Если ProvideValue возвращает FixupToken, то ProvideValue этого расширения разметки будет вызван ещё раз, когда все требуемые элементы будут созданы. А для отлова случаев, когда искомого элемента вообще нет в разметке, код ProvideValue должен перед вызовом GetFixupToken проверять значение свойства IsFixupTokenAvailable. Если оно равно False, граф объектов уже создан полностью, и метод должен как-то обработать ошибку (выбросить исключение, как правило), если нужного элемента среди созданных объектов нет.

Paginator для админок

Written by elwood

Простая листалка на JSP, подходит для админок. Для использования нужно заинклудить этот файл в основную JSP страницы, а также установить переменные url, total_records, current_page и page_size. url – базовый url для этой страницы (к нему через амперсанд будет дописываться параметр page). total_records – сколько всего записей имеется, current_page – индекс текущей страницы, начиная с нуля. page_size – размер страницы. Можно настроить, сколько страниц отображать слева и справа от текущей (если мы находимся глубоко в середине).

Выглядит примерно так:

pager

<%@page language="java" contentType="text/html; charset=UTF-8" pageEncoding="utf-8" session="false" %>
 
<%--@elvariable id="url" type="java.lang.String"--%>
<%--@elvariable id="total_records" type="java.lang.Integer"--%>
<%--@elvariable id="current_page" type="java.lang.Integer"--%>
<%--@elvariable id="page_size" type="java.lang.Integer"--%>
 
<style type="text/css">
    div.pagination {
        margin-top: 10px;
        text-align: center;
    }
    .pagination a {
        color: #3b5998;
        text-decoration: none;
        border: 1px #c2d1df solid;
        padding: 2px 5px;
        margin-right: 5px;
    }
    .pagination a:hover {
        color: #3b5998;
        text-decoration: none;
        border: 1px #3b5998 solid;
    }
    a.current {
        background: #c2d6ed;
    }
</style>
 
<div class="pagination">
    <a href="${url}&page=${current_page-1>=0?current_page-1:0}" class="prev">&lt; Prev</a>
    <%
        Integer total_records = (Integer) request.getAttribute("total_records");
        Integer page_size = (Integer) request.getAttribute("page_size");
        Integer current_page = (Integer) request.getAttribute( "current_page" );
        int pages = total_records / page_size;
        int lastPage = pages * page_size < total_records ? pages : pages - 1;
        request.setAttribute("last_page", lastPage);
        // сколько ссылок отображается начиная с самой первой (не может быть установлено в 0)
        final int N_PAGES_FIRST = 1;
        // сколько ссылок отображается слева от текущей (может быть установлено в 0)
        final int N_PAGES_PREV = 1;
        // сколько ссылок отображается справа от текущей (может быть установлено в 0)
        final int N_PAGES_NEXT = 1;
        // сколько ссылок отображается в конце списка страниц (не может быть установлено в 0)
        final int N_PAGES_LAST = 1;
        if (N_PAGES_FIRST < 1 || N_PAGES_LAST < 1) throw new AssertionError(  );
        // показывать ли полностью все ссылки на страницы слева от текущей, или вставить многоточие
        boolean showAllPrev;
        // показывать ли полностью все ссылки на страницы справа от текущей, или вставить многоточие
        boolean showAllNext;
        showAllPrev = N_PAGES_FIRST >= (current_page - N_PAGES_PREV);
        showAllNext = current_page + N_PAGES_NEXT >= lastPage - N_PAGES_LAST;
        request.setAttribute( "N_PAGES_FIRST", N_PAGES_FIRST );
        request.setAttribute( "N_PAGES_PREV", N_PAGES_PREV );
        request.setAttribute( "N_PAGES_NEXT", N_PAGES_NEXT );
        request.setAttribute( "N_PAGES_LAST", N_PAGES_LAST );
        request.setAttribute( "showAllPrev", showAllPrev );
        request.setAttribute( "showAllNext", showAllNext );
    %>
    <%-- show left pages --%>
    <c:choose>
        <c:when test="${showAllPrev}">
            <c:if test="${current_page > 0}">
                <c:forEach begin="0" end="${current_page - 1}" var="p">
                    <a href="${url}&page=${p}">${p + 1}</a>
                </c:forEach>
            </c:if>
        </c:when>
        <c:otherwise>
            <c:forEach begin="0" end="${N_PAGES_FIRST - 1}" var="p">
                <a href="${url}&page=${p}">${p + 1}</a>
            </c:forEach>
            <span style="margin-right: 5px">...</span>
            <c:forEach begin="${current_page - N_PAGES_PREV}" end="${current_page - 1}" var="p">
                <a href="${url}&page=${p}">${p + 1}</a>
            </c:forEach>
        </c:otherwise>
    </c:choose>
    <%-- show current page --%>
    <a href="${url}&page=${current_page}" class="current">${current_page + 1}</a>
    <%-- show right pages --%>
    <c:choose>
        <c:when test="${showAllNext}">
            <c:forEach begin="${current_page + 1}" end="${last_page}" var="p">
                <a href="${url}&page=${p}">${p + 1}</a>
            </c:forEach>
        </c:when>
        <c:otherwise>
            <c:forEach begin="${current_page + 1}" end="${current_page + 1 + (N_PAGES_NEXT - 1)}" var="p">
                <a href="${url}&page=${p}">${p + 1}</a>
            </c:forEach>
            <span style="margin-right: 5px">...</span>
            <c:forEach begin="${last_page - (N_PAGES_LAST - 1)}" end="${last_page}" var="p">
                <a href="${url}&page=${p}">${p + 1}</a>
            </c:forEach>
        </c:otherwise>
    </c:choose>
    <a href="${url}&page=${current_page + 1 > last_page ? last_page : current_page + 1}" class="next">Next &gt;</a>
</div>

Инструменты для отладки jQuery events

Written by elwood

Часто бывает нужно определить, какие обработчики навешаны на DOM-элементы. К сожалению, из коробки ни FireBug, ни Chrome Developer Tools не дают возможности удобно ответить на этот вопрос, так как показывают лишь прямые обработчики, которые были повешены присваиванием element.onclick = handler. Если же для этого использовался jQuery, то узнать о наличии обработчиков можно только просканировав дерево элементов до document, просматривая то, что хранится в $._data(element, ‘events’). Для этого я написал небольшой сниппет, который добавил в код сайта:

jquery-events

/**
 * Дампер jquery on- и live-обработчиков.
 * @author elwood
 * 16.10.13 15:43
 */
 
/**
 * Выводит дамп on- и live-обработчиков для указанного объекта и всех его родителей.
 * У тех обработчиков, у которых определён селектор, выводятся также дочерние узлы (относительно
 * элемента, к которому привязан обработчик), удовлетворяющие ему.
 * @param $object
 */
function dumpEvents($object) {
 
    var $current = $object;
    while ($current.length != 0) {
        console.info("processing %o", $current[0]);
        var eventsObject = $._data($current[0], 'events');
        for (var property in eventsObject) {
            console.info(property + ':');
            var handlers = eventsObject[property];
            for (var i = 0; i < handlers.length; i++) {
                if (typeof handlers[i].selector != undefined && handlers[i].selector != null) {
                    console.info("selector: " + handlers[i].selector + ", handler: %O", handlers[i].handler);
                    var $selected = $current.find(handlers[i].selector);
                    console.info("selected %d children:", $selected.length);
                    for (var j = 0; j < $selected.length; j++) {
                        console.info(">> %o", $selected[j]);
                    }
                } else {
                    console.info("handler: %O", handlers[i].handler);
                }
            }
        }
 
        $current = $current.parent();
    }
}
 
/**
 * Вариант использования с xpath (для удобства при работе в хроме).
 * @param xpath
 */
function dumpEventsX(xpath) {
    var $object = $(document).xpath(xpath);
    dumpEvents($object);
}
 
/**
 * Выводит live-обработчики, глобально привязанные к объекту document.
 */
function dumpDocumentEvents() {
    dumpEvents($(document));
}

Для работы dumpEventsX() необходимо наличие плагина jQuery.XPath. XPath я использовал для того, чтобы из панели Elements в хроме правой кнопкой можно было делать Copy XPath и потом в консоли вставлять этот xpath в качестве аргумента функции dumpEventsX().

Честно говоря, я глубоко не разбирался в том, как реализована подписка обработчиков с помощью jQuery, и есть ли другие варианты, которые работают иначе. Вроде бы, bind() и click() делают то же самое, только с конечным объектом (селектор у хендлера будет пустой, и на потомков хендлер уже не будет работать).

Параллельно с тем, как я ковырялся в этих зарослях, нашёл несколько плагинов, делающих примерно то же самое.

Для Сhrome

jQuery Debugger – в Elements появляются вкладки jQuery Data и jQuery Events для каждого DOM-элемента можно посмотреть, что к нему джикверивского привязано, очень удобное расширение

Вот как это выглядит:
jquery-debugger

Visual Event – очень красивый плагин, при нажатии на кнопку показывает все элементы с привязанными к ним событиями и кодом их обработчиков. Поддерживает события jQuery.
Event Spy – тоже полезная штука, но этот плагин, судя по всему, умеет работать только с непосредственно привязанными обработчиками. Код обработчиков тоже выводится, довольно удобно.

Для FireFox

FireQuery– аддон к FireBug, показывает данные, привязанные к DOM-элементам (правда, не очень удобная навигация) – полезный аддон
EventBug – тоже аддон к FireBug, добавляет вкладку Events к броузеру DOM-элементов
Visual Event – то же самое, что и в хроме, только для FireFox он выполнен в виде bookmarklet, то есть ссылки, которую нужно добавить в закладки, и нажимать когда работаешь со страницей. Наверное, и в Опере работает.