自定义组件,即 UIInput 和 UICommand
Custom Component that is UIInput and UICommand
在处理 JSF 自定义组件时遇到了一点困难。如果有人可以让我抢先一步就好了。
我的想法是拥有一个组件,让我可以通过 LocalDate
进行分页。想想一个更简单的日期选择器。呈现的 HTML 有两个按钮,一个将日期递增一天,另一个按钮将日期递减一天。该值本身存储在隐藏的输入中。附件是该组件的一个简单版本,它不起作用,但希望能描述我正在努力实现的目标。
想法是在页面上说 <my:datePager value="#{myBackingBean.someDate}" />
并在单击任一按钮后执行一些 logic/action。
例如<my:datePager value="#{myBackingBean.someDate} action="..." />
或<my:datePager value="#{myBackingBean.someDate} previous="..." next="..."/>
不知道哪个更好。
这是我一直坚持的问题:How to call the previous
and next
methods on the component?
到目前为止,这是我的 JSF 2.3 自定义组件:
@FacesComponent(LocalDatePager.COMPONENT_TYPE)
public class LocalDatePager extends UIInput {
public static final String COMPONENT_TYPE = "LocalDatePager";
public LocalDatePager() {
setConverter(LocalDateConverter.INSTANCE);
}
public void previous() {
LocalDate localDate = (LocalDate) getValue();
localDate = localDate.minusDays(1);
setValue(localDate);
}
public void next() {
LocalDate localDate = (LocalDate) getValue();
localDate = localDate.plusDays(1);
setValue(localDate);
}
@Override
public void decode(FacesContext context) {
super.decode(context);
}
@Override
public void encodeEnd(FacesContext context) throws IOException {
ResponseWriter writer = context.getResponseWriter();
writer.startElement("div", this);
writer.writeAttribute("class", "btn-group", null);
writer.writeAttribute("role", "group", null);
writer.startElement("button", this);
writer.writeAttribute("type", "button", null);
writer.writeAttribute("class", "btn btn-outline-primary", null);
writer.writeText("Previous", null);
writer.endElement("button");
writer.startElement("button", this);
writer.writeAttribute("type", "button", null);
writer.writeAttribute("class", "btn btn-outline-primary", null);
writer.writeText("Next", null);
writer.endElement("button");
writer.endElement("div");
writer.startElement("input", this);
writer.writeAttribute("name", getClientId(context), "clientId");
writer.writeAttribute("type", "hidden", null);
writer.writeAttribute("value", getValue(), "value");
writer.endElement("input");
}
}
根据我的理解,我所拥有的组成部分是头脑,它是 UIInput
和 UICommand
。我需要实施 ActionEvent
吗?火值变化事件?
目前我在支持 bean 中而不是在组件中执行整个上一个和下一个方法。这会导致大量重复代码,我认为自定义组件最合适。我尝试使用复合组件,但这也没有让我走得太远。
到目前为止,另一种方法是客户端行为:执行 JavaScript,更改隐藏输入的值并提交表单。这感觉更糟。
如果有人能给我指点正确的方向就好了。
作为UIInput
首先,您需要在 encodeXxx()
方法中将按钮呈现为正常的提交按钮。此外,您宁愿为此使用 encodeAll()
而不是 encodeEnd()
,因为这似乎是一个“叶”组件,因此您不想处理 children。实施 encodeAll()
会更有效率。将现有的 encodeEnd()
方法重命名为 encodeAll()
并调整按钮的编码如下:
writer.startElement("button", this);
writer.writeAttribute("type", "submit", null); // instead of "button"
writer.writeAttribute("name", getClientId(context) + "_previous", "clientId");
writer.writeText("Previous", null);
// ...
writer.startElement("button", this);
writer.writeAttribute("type", "submit", null); // instead of "button"
writer.writeAttribute("name", getClientId(context) + "_next", "clientId");
writer.writeText("Next", null);
// ...
// The hidden input field in your existing code is fine as-is.
然后你可以在请求参数映射中检查它们。如果你想让你的组件保持 UIInput
,那么我建议在 getConvertedValue()
方法中执行该工作:
@Override
protected Object getConvertedValue(FacesContext context, Object submittedValue) throws ConverterException {
LocalDate localDate = (LocalDate) super.getConvertedValue(context, submittedValue);
if (localDate != null) {
Map<String, String> params = context.getExternalContext().getRequestParameterMap();
if (params.get(getClientId(context) + "_previous") != null) {
localDate = localDate.minusDays(1);
}
else if (params.get(getClientId(context) + "_next") != null) {
localDate = localDate.plusDays(1);
}
}
return localDate;
}
然后您可以简单地在值更改侦听器中完成这项工作:
<my:datePager value="#{bean.localDate}" valueChangeListener="#{bean.onpage}" />
public void onpage(ValueChangeEvent event) {
// ...
}
基本上就这些了。无需手动触发值更改事件。 JSF 已经处理了其余的逻辑。
但是,这种方法的缺点是它可能会在 JSF 生命周期的错误时刻被调用。在验证阶段调用值更改侦听器,而您确实希望在调用应用程序阶段调用它。想象一下,这个按钮放置在一个状态取决于与 #{bean.localDate}
关联的数据的表单中,那么它们的更新模型值阶段可能会出错,因为 localDate
更改得太早了。
作为UICommand
如果上述缺点是一个真正的问题,虽然有work around,但你最好将UIInput
转换为UICommand
。不可能两者兼而有之。你选择一个或另一个。我的个人建议是选择 UICommand
,因为它在 JSF 生命周期中的行为“更自然”,同时牢记组件的唯一目的(“分页到 next/previous 日期”)。以下是步骤:
将 extends UIInput
换成 extends UICommand
。
删除构造函数中的 setConverter()
调用。
保留encodeAll()
方法。完全没问题。
删除getConvertedValue()
方法并实现decode()
如下:
@Override
public void decode(FacesContext context) {
Map<String, String> params = context.getExternalContext().getRequestParameterMap();
if (params.get(getClientId(context) + "_previous") != null) {
queueEvent(new ActionEvent(context, this));
}
else if (params.get(getClientId(context) + "_next") != null) {
queueEvent(new ActionEvent(context, this));
}
}
这些 queueEvent()
调用将导致调用同一组件的 broadcast()
。所以覆盖它如下:
@Override
public void broadcast(FacesEvent event) throws AbortProcessingException {
FacesContext context = event.getFacesContext();
Map<String, String> params = context.getExternalContext().getRequestParameterMap();
LocalDate localDate = LocalDate.parse(params.get(getClientId())); // TODO: gracefully handle any conversion error.
if (params.get(getClientId(context) + "_previous") != null) {
localDate = localDate.minusDays(1);
}
else if (params.get(getClientId(context) + "_next") != null) {
localDate = localDate.plusDays(1);
}
getValueExpression("value").setValue(context.getELContext(), localDate);
super.broadcast(event); // Invokes bean method.
}
请注意 value
属性是如何在模型中手动解码、转换和更新的。虽然这是作为隐藏输入值传递的,但您可能希望添加一些逻辑来妥善处理 TODO
.
中指示的任何转换错误
现在您可以使用它了:
<my:datePager value="#{bean.localDate}" action="#{bean.onpage}" />
public void onpage() {
// ...
}
在处理 JSF 自定义组件时遇到了一点困难。如果有人可以让我抢先一步就好了。
我的想法是拥有一个组件,让我可以通过 LocalDate
进行分页。想想一个更简单的日期选择器。呈现的 HTML 有两个按钮,一个将日期递增一天,另一个按钮将日期递减一天。该值本身存储在隐藏的输入中。附件是该组件的一个简单版本,它不起作用,但希望能描述我正在努力实现的目标。
想法是在页面上说 <my:datePager value="#{myBackingBean.someDate}" />
并在单击任一按钮后执行一些 logic/action。
例如<my:datePager value="#{myBackingBean.someDate} action="..." />
或<my:datePager value="#{myBackingBean.someDate} previous="..." next="..."/>
不知道哪个更好。
这是我一直坚持的问题:How to call the previous
and next
methods on the component?
到目前为止,这是我的 JSF 2.3 自定义组件:
@FacesComponent(LocalDatePager.COMPONENT_TYPE)
public class LocalDatePager extends UIInput {
public static final String COMPONENT_TYPE = "LocalDatePager";
public LocalDatePager() {
setConverter(LocalDateConverter.INSTANCE);
}
public void previous() {
LocalDate localDate = (LocalDate) getValue();
localDate = localDate.minusDays(1);
setValue(localDate);
}
public void next() {
LocalDate localDate = (LocalDate) getValue();
localDate = localDate.plusDays(1);
setValue(localDate);
}
@Override
public void decode(FacesContext context) {
super.decode(context);
}
@Override
public void encodeEnd(FacesContext context) throws IOException {
ResponseWriter writer = context.getResponseWriter();
writer.startElement("div", this);
writer.writeAttribute("class", "btn-group", null);
writer.writeAttribute("role", "group", null);
writer.startElement("button", this);
writer.writeAttribute("type", "button", null);
writer.writeAttribute("class", "btn btn-outline-primary", null);
writer.writeText("Previous", null);
writer.endElement("button");
writer.startElement("button", this);
writer.writeAttribute("type", "button", null);
writer.writeAttribute("class", "btn btn-outline-primary", null);
writer.writeText("Next", null);
writer.endElement("button");
writer.endElement("div");
writer.startElement("input", this);
writer.writeAttribute("name", getClientId(context), "clientId");
writer.writeAttribute("type", "hidden", null);
writer.writeAttribute("value", getValue(), "value");
writer.endElement("input");
}
}
根据我的理解,我所拥有的组成部分是头脑,它是 UIInput
和 UICommand
。我需要实施 ActionEvent
吗?火值变化事件?
目前我在支持 bean 中而不是在组件中执行整个上一个和下一个方法。这会导致大量重复代码,我认为自定义组件最合适。我尝试使用复合组件,但这也没有让我走得太远。
到目前为止,另一种方法是客户端行为:执行 JavaScript,更改隐藏输入的值并提交表单。这感觉更糟。
如果有人能给我指点正确的方向就好了。
作为UIInput
首先,您需要在 encodeXxx()
方法中将按钮呈现为正常的提交按钮。此外,您宁愿为此使用 encodeAll()
而不是 encodeEnd()
,因为这似乎是一个“叶”组件,因此您不想处理 children。实施 encodeAll()
会更有效率。将现有的 encodeEnd()
方法重命名为 encodeAll()
并调整按钮的编码如下:
writer.startElement("button", this);
writer.writeAttribute("type", "submit", null); // instead of "button"
writer.writeAttribute("name", getClientId(context) + "_previous", "clientId");
writer.writeText("Previous", null);
// ...
writer.startElement("button", this);
writer.writeAttribute("type", "submit", null); // instead of "button"
writer.writeAttribute("name", getClientId(context) + "_next", "clientId");
writer.writeText("Next", null);
// ...
// The hidden input field in your existing code is fine as-is.
然后你可以在请求参数映射中检查它们。如果你想让你的组件保持 UIInput
,那么我建议在 getConvertedValue()
方法中执行该工作:
@Override
protected Object getConvertedValue(FacesContext context, Object submittedValue) throws ConverterException {
LocalDate localDate = (LocalDate) super.getConvertedValue(context, submittedValue);
if (localDate != null) {
Map<String, String> params = context.getExternalContext().getRequestParameterMap();
if (params.get(getClientId(context) + "_previous") != null) {
localDate = localDate.minusDays(1);
}
else if (params.get(getClientId(context) + "_next") != null) {
localDate = localDate.plusDays(1);
}
}
return localDate;
}
然后您可以简单地在值更改侦听器中完成这项工作:
<my:datePager value="#{bean.localDate}" valueChangeListener="#{bean.onpage}" />
public void onpage(ValueChangeEvent event) {
// ...
}
基本上就这些了。无需手动触发值更改事件。 JSF 已经处理了其余的逻辑。
但是,这种方法的缺点是它可能会在 JSF 生命周期的错误时刻被调用。在验证阶段调用值更改侦听器,而您确实希望在调用应用程序阶段调用它。想象一下,这个按钮放置在一个状态取决于与 #{bean.localDate}
关联的数据的表单中,那么它们的更新模型值阶段可能会出错,因为 localDate
更改得太早了。
作为UICommand
如果上述缺点是一个真正的问题,虽然有work around,但你最好将UIInput
转换为UICommand
。不可能两者兼而有之。你选择一个或另一个。我的个人建议是选择 UICommand
,因为它在 JSF 生命周期中的行为“更自然”,同时牢记组件的唯一目的(“分页到 next/previous 日期”)。以下是步骤:
将
extends UIInput
换成extends UICommand
。删除构造函数中的
setConverter()
调用。保留
encodeAll()
方法。完全没问题。删除
getConvertedValue()
方法并实现decode()
如下:@Override public void decode(FacesContext context) { Map<String, String> params = context.getExternalContext().getRequestParameterMap(); if (params.get(getClientId(context) + "_previous") != null) { queueEvent(new ActionEvent(context, this)); } else if (params.get(getClientId(context) + "_next") != null) { queueEvent(new ActionEvent(context, this)); } }
这些
queueEvent()
调用将导致调用同一组件的broadcast()
。所以覆盖它如下:@Override public void broadcast(FacesEvent event) throws AbortProcessingException { FacesContext context = event.getFacesContext(); Map<String, String> params = context.getExternalContext().getRequestParameterMap(); LocalDate localDate = LocalDate.parse(params.get(getClientId())); // TODO: gracefully handle any conversion error. if (params.get(getClientId(context) + "_previous") != null) { localDate = localDate.minusDays(1); } else if (params.get(getClientId(context) + "_next") != null) { localDate = localDate.plusDays(1); } getValueExpression("value").setValue(context.getELContext(), localDate); super.broadcast(event); // Invokes bean method. }
请注意
中指示的任何转换错误value
属性是如何在模型中手动解码、转换和更新的。虽然这是作为隐藏输入值传递的,但您可能希望添加一些逻辑来妥善处理TODO
.
现在您可以使用它了:
<my:datePager value="#{bean.localDate}" action="#{bean.onpage}" />
public void onpage() {
// ...
}