Не так давно я решил лично узнать, за что все кому не лень ругают 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 выглядит значительно привлекательнее.
Комментариев нет:
Отправить комментарий