[英]UISelectMany in ui:repeat causes java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to java.util.List
I have used the HashMap
method for binding a list of checkboxes to a Map<String, Boolean>
with success. 我已经使用
HashMap
方法将复选框列表成功绑定到Map<String, Boolean>
。 This is nice since it allows you to have a dynamic number of checkboxes. 很好,因为它允许您动态地显示复选框。
I'm trying to extend that to a variable length list of selectManyMenu
. 我试图将其扩展到
selectManyMenu
的可变长度列表。 Being that they are selectMany, I'd like to be able to bind to a Map<String, List<MyObject>>
. 由于它们是selectMany,因此我希望能够绑定到
Map<String, List<MyObject>>
。 I have a single example working where I can bind a single selectManyMenu
to a List<MyObject>
and everything works fine, but whey I put a dynamic number of selectManyMenus inside a ui:repeat
and attempt to bind to the map, I end up with weird results. 我有一个单独的示例,可以将单个
selectManyMenu
绑定到List<MyObject>
并且一切正常,但是乳清我在ui:repeat
放置了动态数量的selectManyMenus并尝试绑定到地图,最终结果很奇怪。 The values are stored correctly in the map, as verified by the debugger, and calling toString()
, but the runtime thinks the map's values are of type Object
and not List<MyObject>
and throws ClassCastExceptions when I try to access the map's keys. 值已正确地存储在映射中,已由调试器验证,并调用
toString()
,但是运行时认为映射的值是Object
类型而不是List<MyObject>
的类型,并在尝试访问映射的键时抛出ClassCastExceptions。
I'm guessing it has something to do with how JSF determines the runtime type of the target of your binding, and since I am binding to a value in a Map
, it doesn't know to get the type from the value type parameter of the map. 我猜想这与JSF如何确定绑定目标的运行时类型有关,并且由于我绑定到
Map
的值,因此不知道从的值类型参数获取类型地图。 Is there any workaround to this, other than probably patching Mojarra? 除了修补Mojarra以外,是否有其他解决方法?
In general, how can I have a page with a dynamic number of selectManyMenus? 总的来说,我怎么能有一个带有动态数量的selectManyMenus的页面? Without, of course using Primefaces'
<p:solveThisProblemForMe>
component. 没有,当然要使用Primefaces的
<p:solveThisProblemForMe>
组件。 (In all seriousness, Primefaces is not an option here, due to factors outside of my control.) (实际上,由于我无法控制的因素,Primefaces在这里不是一个选择。)
The question UISelectMany on a List<T> causes java.lang.ClassCastException: java.lang.String cannot be cast to T had some good information that I wasn't aware of, but I'm still having issues with this SSCE: List <T>上的问题UISelectMany导致java.lang.ClassCastException:无法将java.lang.String强制转换为T ,虽然我不知道有一些很好的信息,但是此SSCE仍然存在问题:
JSF: JSF:
<ui:define name="content">
<h:form>
<ui:repeat value="#{testBean.itemCategories}" var="category">
<h:selectManyMenu value="#{testBean.selectedItemMap[category]}">
<f:selectItems value="#{testBean.availableItems}" var="item" itemValue="#{item}" itemLabel="#{item.name}"></f:selectItems>
<f:converter binding="#{itemConverter}"></f:converter>
<f:validator validatorId="test.itemValidator"></f:validator>
</h:selectManyMenu>
</ui:repeat>
<h:commandButton value="Submit">
<f:ajax listener="#{testBean.submitSelections}" execute="@form"></f:ajax>
</h:commandButton>
</h:form>
</ui:define>
Converter: 转换器:
@Named
public class ItemConverter implements Converter {
@Inject
ItemStore itemStore;
@Override
public Object getAsObject(FacesContext context, UIComponent component, String value) {
return itemStore.getById(value);
}
@Override
public String getAsString(FacesContext context, UIComponent component, Object value) {
return Optional.of(value)
.filter(v -> Item.class.isInstance(v))
.map(v -> ((Item) v).getId())
.orElse(null);
}
}
Backing Bean: 后备豆:
@Data
@Slf4j
@Named
@ViewScoped
public class TestBean implements Serializable {
private static final long serialVersionUID = 1L;
@Inject
ItemStore itemStore;
List<Item> availableItems;
List<String> itemCategories;
Map<String, List<Item>> selectedItemMap = new HashMap<>();
public void initialize() {
log.debug("Initialized TestBean");
availableItems = itemStore.getAllItems();
itemCategories = new ArrayList<>();
itemCategories.add("First Category");
itemCategories.add("Second Category");
itemCategories.add("Third Category");
}
public void submitSelections(AjaxBehaviorEvent event) {
log.debug("Submitted Selections");
selectedItemMap.entrySet().forEach(entry -> {
String key = entry.getKey();
List<Item> items = entry.getValue();
log.debug("Key: {}", key);
items.forEach(item -> {
log.debug(" Value: {}", item);
});
});
}
}
ItemStore just contains a HashMap and delegate methods to access Items by their ID field. ItemStore仅包含HashMap和委托方法,以按其ID字段访问Item。
Item: 项目:
@Data
@Builder
public class Item {
private String id;
private String name;
private String value;
}
ItemListValidator: ItemListValidator:
@FacesValidator("test.itemValidator")
public class ItemListValidator implements Validator {
@Override
public void validate(FacesContext context, UIComponent component, Object value) throws ValidatorException {
if (List.class.isInstance(value)) {
if (((List) value).size() < 1) {
throw new ValidatorException(new FacesMessage(FacesMessage.SEVERITY_FATAL, "You must select at least 1 Admin Area", "You must select at least 1 Admin Area"));
}
}
}
}
Error: 错误:
java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to java.util.List
Stacktrace snipped but occurs on this line: Stacktrace被截断,但发生在此行:
List<Item> items = entry.getValue();
What am I missing here? 我在这里想念什么?
As hinted in the related question UISelectMany on a List<T> causes java.lang.ClassCastException: java.lang.String cannot be cast to T , generic type arguments are unavailable during runtime. 如List <T>上的相关问题UISelectMany所提示的, 导致java.lang.ClassCastException:无法将java.lang.String强制转换为T ,泛型类型参数在运行时不可用。 In other words, EL doesn't know you have a
Map<String, List<Item>>
. 换句话说,EL不知道您具有
Map<String, List<Item>>
。 All EL knows is that you have a Map
, so unless you explicitly specify a converter for the selected values, and a collection type for the collection, JSF will default to String
for selected values and an object array Object[]
for the collection. EL所知道的就是您拥有
Map
,因此,除非您为选定的值明确指定一个转换器,并为该集合指定一个收集类型,否则JSF对于选定的值将默认为String
,对于集合的对象数组为Object[]
。 Do note that the [
in [Ljava.lang.Object
indicates an array. 请注意,
[
在[Ljava.lang.Object
表示数组。
Given that you want the collection type to be an instance of java.util.List
, you need to specify the collectionType
attribute with the FQN of the desired concrete implementation. 假设您希望集合类型是
java.util.List
的实例,则需要使用所需具体实现的FQN指定collectionType
属性。
<h:selectManyMenu ... collectionType="java.util.ArrayList">
JSF will then make sure that the right collection type is being instantiated in order to fill the selected items and put in the model. 然后,JSF将确保实例化正确的集合类型,以填充选定的项目并放入模型中。 Here's a related question where such a solution is being used but then for a different reason: org.hibernate.LazyInitializationException at com.sun.faces.renderkit.html_basic.MenuRenderer.convertSelectManyValuesForModel .
这是一个相关的问题,其中使用了这样的解决方案,但随后又有不同的原因: com.sun.faces.renderkit.html_basic.MenuRenderer.convertSelectManyValuesForModel上的org.hibernate.LazyInitializationException 。
Update : I should have tested the above theory. 更新 :我应该已经验证了上述理论。 This doesn't work in Mojarra when the collection behind
collectionType
is in turn wrapped in another generic collection/map. 当collectionType之后的
collectionType
又包装在另一个通用集合/映射中时,这在Mojarra中不起作用。 Mojarra only checks the collectionType
if the UISelectMany
value itself already represents an instance of java.util.Collection
. Mojarra仅在
UISelectMany
值本身已经表示java.util.Collection
的实例时检查collectionType
。 However, due to it being wrapped in a Map
, its (raw) type becomes java.lang.Object
and then Mojarra will skip the check for any collectionType
. 但是,由于将其包装在
Map
,其(原始)类型变为java.lang.Object
,然后Mojarra将跳过对任何collectionType
的检查。
MyFaces did a better job in this in its UISelectMany
renderer, it works over there. MyFaces在其
UISelectMany
渲染器中做得更好,可以在那工作。
As far as I inspected Mojarra's source code, there's no way to work around this other way than replacing Map<String, List<Long>>
by a List<Category>
where Category
is a custom object having String name
and List<MyObject> selectedItems
properties. 就我检查Mojarra的源代码而言,除了用
List<Category>
替换Map<String, List<Long>>
,没有其他方法可以解决此问题Map<String, List<Long>>
其中Category
是具有String name
和List<MyObject> selectedItems
的自定义对象属性。 True, this really kills the advantage of Map
of having dynamic keys in EL, but it is what it is. 的确,这确实消除了
Map
在EL中具有动态键的优势,但事实就是如此。
Here's a MCVE using Long
as item type (just substitute it with your MyObject
): 这是使用
Long
作为项目类型的MCVE (只需将其替换为MyObject
):
private List<Category> categories;
private List<Long> availableItems;
@PostConstruct
public void init() {
categories = Arrays.asList(new Category("one"), new Category("two"), new Category("three"));
availableItems = Arrays.asList(1L, 2L, 3L, 4L, 5L);
}
public void submit() {
categories.forEach(c -> {
System.out.println("Name: " + c.getName());
for (Long selectedItem : c.getSelectedItems()) {
System.out.println("Selected item: " + selectedItem);
}
});
// ...
}
public class Category {
private String name;
private List<Long> selectedItems;
public Category(String name) {
this.name = name;
}
// ...
}
<h:form>
<ui:repeat value="#{bean.categories}" var="category">
<h:selectManyMenu value="#{category.selectedItems}" converter="javax.faces.Long">
<f:selectItems value="#{bean.availableItems}" />
</h:selectManyMenu>
</ui:repeat>
<h:commandButton value="submit" action="#{bean.submit}">
<f:ajax execute="@form" />
</h:commandButton>
</h:form>
Do note that collectionType
is unnecessary here. 请注意,此处不需要
collectionType
。 Only the converter
is still necessary. 仍然只需要
converter
。
Unrelated to the concrete problem, I'd like to point out that selectedItemMap.entrySet().forEach(entry -> { String key ...; List<Item> items ...;})
can be simplified to selectedItemMap.forEach((key, items) -> {})
and that ItemListValidator
is unnecessary if you just use required="true"
on the input component. 与具体问题无关 ,我想指出的是,
selectedItemMap.entrySet().forEach(entry -> { String key ...; List<Item> items ...;})
可以简化为selectedItemMap.forEach((key, items) -> {})
,如果仅在输入组件上使用required="true"
,则不需要ItemListValidator
。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.