Не так давно я решил лично узнать, за что все кому не лень ругают JSF. Касаемо web-разработки, на тот момент у меня был лишь некоторый опыт работы с фреймворком ZK (его создатели, кстати тоже хают JSF и некоторых другие фреймворки, но в данном случае делают они это скорее в маркетинговых целях).
Собравшись изучать JSF (разумеется версию 2.x), я выбрал достаточно суровую, на мой взгляд, библиотеку компонентов - RichFaces (последней стабильной версией на тот момент была 4.3.4.Final, в исходниках же к статье используется версия 4.3.7.Final - принципиальных различий нет) - и через недолгое время знакомства с ней столкнулся с элементарной, казалось бы, проблемой: по щелчку на кнопке/ссылке показать пользователю модальный диалог с каким-нибудь вопросом "Do you really want to do something?", а также парой кнопок OK и Cancel. В принципе, для такого простого случая взаимодействия с пользователем можно было бы обойтись и JavaScript-функцией confirm, но что если заказчику или вам самим захочется чего-то особенного. К примеру, чтобы окно диалога было выполнено в определённом стиле. В общем, стандартного компонента в RichFaces для этой цели нет, а в интернетах предлагается решение на основе JavaScript, отображающего скрытый модальный диалог. Но мне больше понравился подход PrimeFaces. В этой библиотеке есть готовый компонент confirmDialog. Взгляните на демку, чтобы ознакомиться с его работой. Принцип действия схож с решениями для RichFaces, однако выглядит всё более целостно и возможно такой подход поможет немного снизить степень гремучести смеси "html/el/javascript и еще черт знает чего" ©, из которой состоят представления в JSF. Ниже приведено краткое руководство по созданию в RichFaces аналога компонента confirmDialog (тут исходники).
Алгоритм работы диалогового окна подтверждения (далее просто - диалога) примерно таков:
Собравшись изучать JSF (разумеется версию 2.x), я выбрал достаточно суровую, на мой взгляд, библиотеку компонентов - RichFaces (последней стабильной версией на тот момент была 4.3.4.Final, в исходниках же к статье используется версия 4.3.7.Final - принципиальных различий нет) - и через недолгое время знакомства с ней столкнулся с элементарной, казалось бы, проблемой: по щелчку на кнопке/ссылке показать пользователю модальный диалог с каким-нибудь вопросом "Do you really want to do something?", а также парой кнопок OK и Cancel. В принципе, для такого простого случая взаимодействия с пользователем можно было бы обойтись и JavaScript-функцией confirm, но что если заказчику или вам самим захочется чего-то особенного. К примеру, чтобы окно диалога было выполнено в определённом стиле. В общем, стандартного компонента в RichFaces для этой цели нет, а в интернетах предлагается решение на основе JavaScript, отображающего скрытый модальный диалог. Но мне больше понравился подход PrimeFaces. В этой библиотеке есть готовый компонент confirmDialog. Взгляните на демку, чтобы ознакомиться с его работой. Принцип действия схож с решениями для RichFaces, однако выглядит всё более целостно и возможно такой подход поможет немного снизить степень гремучести смеси "html/el/javascript и еще черт знает чего" ©, из которой состоят представления в JSF. Ниже приведено краткое руководство по созданию в RichFaces аналога компонента confirmDialog (тут исходники).
Алгоритм работы диалогового окна подтверждения (далее просто - диалога) примерно таков:
- с помощью особого поведения (behavior), добавленного к кнопке или ссылке, по щелчку на которой мы хотим показывать диалог, припрятать стандартный обработчик события onclick в data-атрибуте элемента кнопки/ссылки, а вместо него вставить вызов нашего диалога;
- по щелчку на вышеупомянутой кнопке/ссылке найти на странице заблаговременно размещённый диалог и показать его пользователю;
- по щелчку на кнопке OK в диалоге взять кнопку/ссылку, вызвавшую этот диалог, получить стандартный обработчик события onclick из того самого data-атрибута и выполнить этот обработчик, спрятав диалог до следующего вызова;
- по щелчку на кнопке Cancel просто закрыть диалог.
Всё просто и начать стоит с подготовки компонентов-инициаторов вызова диалога. Перво-наперво нам понадобится особый интерфейс, говорящий о том, что реализующий его компонент способен вызвать диалог:
public interface Confirmable { public String getConfirmationScript(); public void setConfirmationScript(String confirmationScript); public boolean requiresConfirmation(); }
Методами getConfirmationScript/setConfirmationScript можно получить/сохранить специфичный для текущего компонента JavaScript, вызывающий диалог.
А вот кнопка на основе компонента org.richfaces.component.UICommandButton, реализующая интерфейс Confimable (про ссылки писать не буду - там всё делается аналогично):
@FacesComponent("org.positivefaces.CommandButton") public class UICommandButton extends org.richfaces.component.UICommandButton implements Confirmable { private ConfirmationScriptHolder confirmScriptHolder = new ConfirmationScriptHolder(); @Override public String getConfirmationScript() { return confirmScriptHolder.getConfirmationScript(); } @Override public void setConfirmationScript(String confirmationScript) { confirmScriptHolder.setConfirmationScript(confirmationScript); } @Override public boolean requiresConfirmation() { return confirmScriptHolder.requiresConfirmation(); } }Класс ConfirmationScriptHolder призван чуточку сократить дублирование кода при реализации интерфейса Confirmable в каком-нибудь другом компоненте:
public class ConfirmationScriptHolder { private String confirmationScript; public String getConfirmationScript() { return confirmationScript; } public void setConfirmationScript(String confirmationScript) { this.confirmationScript = confirmationScript; } public boolean requiresConfirmation() { return !Strings.isNullOrEmpty(confirmationScript); } }
Несколько сомнительное решение, но перфекционизм не даёт покоя.
Теперь нашей кнопке нужен рендерер:
@FacesRenderer(rendererType = "org.positivefaces.CommandButtonRenderer", componentFamily = UICommand.COMPONENT_FAMILY) public class CommandButtonRenderer extends org.richfaces.renderkit.html.CommandButtonRenderer { @Override public void doEncodeEnd(ResponseWriter writer, FacesContext context, UIComponent component) throws IOException { super.doEncodeEnd(new ConfirmCommandResponseWriter(writer, (UICommandButton) component), context, component); } @Override protected Class<? extends UIComponent> getComponentClass() { return UICommandButton.class; } }
За время знакомства с JSF у меня сложилось впечатление, что легко расширить нужный компонент или рендерер - большая удача (в ZK, к слову, с этим тоже не всё гладко). Пару раз я встречал обходные решения на основе обёртки над ResponseWriter'ом. Этот случай не исключение:
public class ConfirmCommandResponseWriter extends ResponseWriterWrapper { private ResponseWriter originalWriter; private Confirmable confirmableCommand; public ConfirmCommandResponseWriter(ResponseWriter originalWriter, Confirmable confirmableCommand) { this.originalWriter = originalWriter; this.confirmableCommand = confirmableCommand; } @Override public ResponseWriter getWrapped() { return originalWriter; } @Override public void writeAttribute(String name, Object value, String property) throws IOException { if (HtmlConstants.ONCLICK_ATTRIBUTE.equals(name) && confirmableCommand.requiresConfirmation()) { super.writeAttribute("data-pfconfirmcommand", value, null); value = confirmableCommand.getConfirmationScript(); } super.writeAttribute(name, value, property); } }
С помощью этой обёртки мы перехватываем запись атрибута onclick для нашей кнопки и заменяем стандартный обработчик на вызов диалога (пункт 1 в алгоритме работы всего этого добра). Не очень красиво, но от копипаста методов на 100+ строк кода мне становится нехорошо.
В принципе, компонент готов. Остаётся только зарегистрировать его в библиотеке тегов и мы переходим к реализации поведения:
@FacesBehavior("org.positivefaces.behavior.ConfirmBehavior") @ResourceDependency(library = "org.positivefaces", name = "js/positivefaces.js") public class ConfirmBehavior extends ClientBehavior { @Override public String getScript(ClientBehaviorContext behaviorContext) { FacesContext context = behaviorContext.getFacesContext(); UIComponent component = behaviorContext.getComponent(); String source = component.getClientId(context); if (component instanceof Confirmable) { String script = "PositiveFaces.confirm({" + "source:'" + source + "'," + "sourceEvent:event," + "header:'" + getHeader() + "'," + "message:'" + getMessage() + "'," + "icon:'" + getIcon() + "'" + "});return false;"; ((Confirmable) component).setConfirmationScript(script); return null; } throw new FacesException("Component " + source + " is not " + "\"confirmable\". ConfirmBehavior can only be attached to " + "component that implements " + Confirmable.class.getName() + " interface"); } ... }
Код этого класса практически эквивалентен коду класса org.primefaces.behavior.confirm.ConfirmBehavior: в целевом компоненте, реализующем интерфейс Confirmable, сохраняется скрипт вызова JavaScript-функции confirm, в которую передаются заголовок диалога, сообщение пользователю и, возможно, ссылка на какую-нибудь картинку. Чудесная JavaScript-функция confirm определена в файле positivefaces.js (от которого, кстати, зависит наш ConfirmBehavior):
Последний ингредиент нашего блюда - сам диалог. Реализация класса компонента довольно скучна (мы просто наследуемся от org.richfaces.component.UIPopupPanel). Рендерер диалога немного интереснее. Его класс - ConfirmDialogRenderer - представляет собой довольно длинную партянку, бОльшая часть кода в которой направлена на предоставление возможности легко улучшить внешний вид диалога с помощью CSS (и действительно, наш диалог с навешанными на него CSS-классами Twitter Bootstrap выглядит, на мой взгляд, весьма недурно). Основная же проблема заключается в необходимости использовать на клиентской стороне JavaScript'овый класс диалога, наследующийся от RichFaces.ui.PopupPanel. И вновь в этом нелёгком деле нам на помощь приходят привычные уже техники подмены ResponseWriter'ов. Здесь мною был опробован ещё более извращённый подход, базирующийся на классе org.ajax4jsf.io.SAXResponseWriter. С помощью его специального наследника, а также внутреннего по отношению к рендереру класса XMLConsumer мы регулярным выражением new\s+([\w.]+)\( отлавливаем тот момент, когда базовый класс записывает вызов конструктора клиентского класса и коварно подменяем его:
На этом всё. Надеюсь, кому-нибудь что-нибудь из этой статьи да и окажется полезным.
Подводя краткий итог недолгому знакомству с JSF, не могу сказать, что это что-то ужасное. Да, иногда тяжеловесно, порой нелогично, а временами просто выносит мозг. Но технология развивается и определённо имеет право на жизнь. Библиотека же RichFaces оказалась действительно суровой. На её фоне та же PrimeFaces выглядит значительно привлекательнее.
(function($, rf) { var pf = window.PositiveFaces = window.PositiveFaces || {}; pf.confirm = function(msg) { if (pf.confirmDialog) { pf.confirmSource = $("#" + msg.source.replace(/:/g, "\\:")); pf.confirmSourceEvent = msg.sourceEvent; pf.confirmDialog.showMessage(msg); } else rf.log.warn('No global confirmation dialog available'); }; })(jQuery, window.RichFaces);Вроде бы всё просто - если на странице есть диалог, выставляем ему кой-какие параметры и являем на свет божий.
Последний ингредиент нашего блюда - сам диалог. Реализация класса компонента довольно скучна (мы просто наследуемся от org.richfaces.component.UIPopupPanel). Рендерер диалога немного интереснее. Его класс - ConfirmDialogRenderer - представляет собой довольно длинную партянку, бОльшая часть кода в которой направлена на предоставление возможности легко улучшить внешний вид диалога с помощью CSS (и действительно, наш диалог с навешанными на него CSS-классами Twitter Bootstrap выглядит, на мой взгляд, весьма недурно). Основная же проблема заключается в необходимости использовать на клиентской стороне JavaScript'овый класс диалога, наследующийся от RichFaces.ui.PopupPanel. И вновь в этом нелёгком деле нам на помощь приходят привычные уже техники подмены ResponseWriter'ов. Здесь мною был опробован ещё более извращённый подход, базирующийся на классе org.ajax4jsf.io.SAXResponseWriter. С помощью его специального наследника, а также внутреннего по отношению к рендереру класса XMLConsumer мы регулярным выражением new\s+([\w.]+)\( отлавливаем тот момент, когда базовый класс записывает вызов конструктора клиентского класса и коварно подменяем его:
@Override public void characters(char[] ch, int start, int length) throws SAXException { if (ch != null && ch.length > 0 && isJavaScript(startedElements.peek())) { String text; if (start == 0 && ch.length == length) text = new String(ch); else text = new String(Arrays.copyOfRange(ch, start, start + length)); Matcher matcher = CLIENT_COMPONENT_CTOR_CALL.matcher(text); if (matcher.find()) { ch = matcher.replaceFirst("new PositiveFaces.ui.ConfirmDialog(").toCharArray(); start = 0; length = ch.length; } } super.characters(ch, start, length); }Код клиентской части диалога скрыт в файле confirmdialog.js. И опять же, по большей части это обеспечение корректного отображения диалога. Самое главное - это обработчик кнопки OK:
this.btnPaneDiv.find('.' + STYLE_CLASS_OK).click(function(e) { if (pf.confirmSource) { var fn = eval('(function(event){' + pf.confirmSource.data('pfconfirmcommand') + '})'); fn.call(pf.confirmSource, pf.confirmSourceEvent); pf.confirmDialog.hide(); pf.confirmSource = null; pf.confirmSourceEvent = null; } e.preventDefault(); });Да, да. eval - это нехорошо, тем не менее, в PrimeFaces сделано именно так. Код, на мой взгляд, кристально ясен: берем у кнопки/ссылки значение атрибута data-pfconfirmcommand и исполняем как JavaScript-функцию, после чего скрываем диалог до следующего раза.
На этом всё. Надеюсь, кому-нибудь что-нибудь из этой статьи да и окажется полезным.
Подводя краткий итог недолгому знакомству с JSF, не могу сказать, что это что-то ужасное. Да, иногда тяжеловесно, порой нелогично, а временами просто выносит мозг. Но технология развивается и определённо имеет право на жизнь. Библиотека же RichFaces оказалась действительно суровой. На её фоне та же PrimeFaces выглядит значительно привлекательнее.
Комментариев нет:
Отправить комментарий