I have multiple selectInputText controls inside a datatable, something like this:
<ice:dataTable id="attributesList" value="#{myForm.myAttributes}" var="entry" cellpadding="0" rows="9999" columnClasses="myColumn,myColumn">
<ice:column>
<!-- auto-complete -->
<ice:panelGroup>
<ice:selectInputText rows="15" width="120" maxlength="255" value="#{entry.attribute.stringValue}" valueChangeListener="#{myFieldAutocomplete.updateList}" immediate="true">
<f:selectItems value="#{myFieldAutocomplete.list}" />
<f:attribute name="fieldName" value="#{entry.name}" />
</ice:selectInputText>
</ice:panelGroup>
</ice:column>
</ice:dataTable>
My backing bean code is this:
public class myFieldAutocomplete {
// default value, empty string
private String currentFieldValue = "";
// list of possible matches.
private List<SelectItem> matchesSIList = new ArrayList<SelectItem>();
public void updateList(ValueChangeEvent event) {
currentFieldValue = "";
// get a new list of matches.
setMatches(event);
if (event.getComponent() instanceof SelectInputText) {
SelectInputText autoComplete = (SelectInputText) event.getComponent();
if (autoComplete.getSelectedItem() != null) {
currentFieldValue = (String) autoComplete.getSelectedItem().getValue();
}
else {
String tempValue = getMatch(autoComplete.getValue().toString());
if(tempValue != null) {
currentFieldValue = tempValue;
}
}
}
}
public String getCurrentFieldValue() {
return currentFieldValue;
}
public List<SelectItem> getList() {
return matchesSIList;
}
private String getMatch(String value) {
String result = null;
if (matchesSIList != null) {
String str;
Iterator<SelectItem> itr = matchesSIList.iterator();
while (itr.hasNext()) {
SelectItem si = (SelectItem) itr.next();
str = (String) si.getValue();
if (str.startsWith(value)) {
result = str;
break;
}
}
}
return result;
}
public void setMatches(ValueChangeEvent event) {
List<String> newMatchesStrList = new ArrayList<String>();
if (event.getComponent() instanceof SelectInputText) {
SelectInputText autoComplete = (SelectInputText) event.getComponent();
myClassDAO myDao = (myClassDAO) Context.getInstance().getBean(myClassDAO.class);
String fieldName = (String) autoComplete.getAttributes().get("fieldName");
newMatchesStrList = myDao.findTemplateFieldValues((String)autoComplete.getValue(), fieldName);
}
// assign new matchList
if (this.matchesSIList != null) {
this.matchesSIList.clear();
}
Iterator<String> itr = newMatchesStrList.iterator();
while(itr.hasNext()) {
String str = (String) itr.next();
matchesSIList.add(new SelectItem(str, str));
}
}
}
The bean is in request scope (although I also tried with session scope). I am using ICEfaces community edition 1.8.2.
The problem is, these auto-complete controls are created dynamically based on certain attributes of each entry in the datatable. So, you can have, for example 2 or more such auto-complete controls in the same panelGroup. When this is the case, when you start typing something the first control, it seems to be triggering the event for all sibling auto-complete controls, and in the end returns the list for the last in order.
In general, I noticed erratic behavior caused by the fact that the event is triggered for all auto-complete controls at once, and values/lists get confused.
What am I doing wrong?
Thanks in advance!
Related
In my ASP.NET MVC Core web application the Json serialization of properties is set to camel case (with first letter lowercase):
services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1)
.AddJsonOptions(opt =>
{
opt.SerializerSettings.ContractResolver = new DefaultContractResolver { NamingStrategy = new CamelCaseNamingStrategy() };
opt.SerializerSettings.Converters.Add(new StringEnumConverter(true));
});
The serialization to the client is working as expected.
But when the javascript client tries to post data and this data is not valid, he receives a validation message with capital letter properties, this validation messages are the ModelState:
{"Info":["The Info field is required."]}
Is there a way to make ASP.NET return lowercase property in validation messages of the ModelState to reflect the naming strategy?
The solution is to disable the automatic api validation filter and create own json result with the validation messages:
services.Configure<ApiBehaviorOptions>(options =>
{
options.SuppressModelStateInvalidFilter = true;
});
And in the controller:
protected ActionResult ValidationFailed()
{
var errorList = ModelState.ToDictionary(
kvp => kvp.Key.ToCamelCase(),
kvp => kvp.Value.Errors.Select(e => e.ErrorMessage).ToArray()
);
return BadRequest(errorList);
}
public async Task<ActionResult> Create([FromBody]TCreateDto model)
{
if (ModelState.IsValid == false)
{
return ValidationFailed();
}
...
}
The string helper method:
public static string ToCamelCase(this string name)
{
if (string.IsNullOrEmpty(name))
{
return name;
}
return name.Substring(0, 1).ToLower() + name.Substring(1);
}
There is an easier solution. Use Fluent Validator's ValidatorOptions.Global.PropertyNameResolver. Taken from here and converted to C# 8 and Fluent Validation 9:
In Startup.cs, ConfigureServices use:
services
.AddControllers()
.SetCompatibilityVersion(CompatibilityVersion.Version_3_0)
.AddFluentValidation(fv =>
{
fv.RegisterValidatorsFromAssemblyContaining<MyValidator>();
// Convert property names to camelCase as Asp.Net Core does https://github.com/FluentValidation/FluentValidation/issues/226
ValidatorOptions.Global.PropertyNameResolver = CamelCasePropertyNameResolver.ResolvePropertyName;
})
.AddNewtonsoftJson(NewtonsoftUtils.SetupNewtonsoftOptionsDefaults);
and resolver itself:
/// <summary>
/// Convert property names to camelCase as Asp.Net Core does
/// https://github.com/FluentValidation/FluentValidation/issues/226
/// </summary>
public class CamelCasePropertyNameResolver
{
public static string? ResolvePropertyName(Type type, MemberInfo memberInfo, LambdaExpression expression)
{
return ToCamelCase(DefaultPropertyNameResolver(type, memberInfo, expression));
}
private static string? DefaultPropertyNameResolver(Type type, MemberInfo memberInfo, LambdaExpression expression)
{
if (expression != null)
{
var chain = PropertyChain.FromExpression(expression);
if (chain.Count > 0)
{
return chain.ToString();
}
}
if (memberInfo != null)
{
return memberInfo.Name;
}
return null;
}
private static string? ToCamelCase(string? s)
{
if (string.IsNullOrEmpty(s) || !char.IsUpper(s[0]))
{
return s;
}
var chars = s.ToCharArray();
for (var i = 0; i < chars.Length; i++)
{
if (i == 1 && !char.IsUpper(chars[i]))
{
break;
}
var hasNext = (i + 1 < chars.Length);
if (i > 0 && hasNext && !char.IsUpper(chars[i + 1]))
{
break;
}
chars[i] = char.ToLower(chars[i], CultureInfo.InvariantCulture);
}
return new string(chars);
}
}
I have faced the same issue. I have overridden DefaultProblemDetailsFactory.cs from the source code and add logic to change the first letters in the 'errors' dictionary.
Steps:
1 - Create new CustomProblemDetailsFactory.cs class:
internal sealed class CustomProblemDetailsFactory : ProblemDetailsFactory
{
private readonly ApiBehaviorOptions _options;
public CustomProblemDetailsFactory(IOptions<ApiBehaviorOptions> options)
{
_options = options?.Value ?? throw new ArgumentNullException(nameof(options));
}
public override ProblemDetails CreateProblemDetails(
HttpContext httpContext,
int? statusCode = null,
string? title = null,
string? type = null,
string? detail = null,
string? instance = null)
{
statusCode ??= 500;
var problemDetails = new ProblemDetails
{
Status = statusCode,
Title = title,
Type = type,
Detail = detail,
Instance = instance,
};
ApplyProblemDetailsDefaults(httpContext, problemDetails, statusCode.Value);
return problemDetails;
}
public override ValidationProblemDetails CreateValidationProblemDetails(
HttpContext httpContext,
ModelStateDictionary modelStateDictionary,
int? statusCode = null,
string? title = null,
string? type = null,
string? detail = null,
string? instance = null)
{
if (modelStateDictionary == null)
{
throw new ArgumentNullException(nameof(modelStateDictionary));
}
statusCode ??= 400;
var problemDetails = new ValidationProblemDetails(modelStateDictionary)
{
Status = statusCode,
Type = type,
Detail = detail,
Instance = instance,
};
if (title != null)
{
// For validation problem details, don't overwrite the default title with null.
problemDetails.Title = title;
}
// FIX LOWERCASE, MAKE THE FIRST LETTERS LOWERCASE
///-----------------------------
if (problemDetails.Errors != null)
{
var newErrors = problemDetails.Errors.ToDictionary(x => this.MakeFirstLetterLowercase(x.Key), x => x.Value);
problemDetails.Errors.Clear();
foreach (var keyValue in newErrors)
{
problemDetails.Errors.Add(keyValue.Key, keyValue.Value);
}
}
///-----------------------------
ApplyProblemDetailsDefaults(httpContext, problemDetails, statusCode.Value);
return problemDetails;
}
private void ApplyProblemDetailsDefaults(HttpContext httpContext, ProblemDetails problemDetails, int statusCode)
{
problemDetails.Status ??= statusCode;
if (_options.ClientErrorMapping.TryGetValue(statusCode, out var clientErrorData))
{
problemDetails.Title ??= clientErrorData.Title;
problemDetails.Type ??= clientErrorData.Link;
}
var traceId = Activity.Current?.Id ?? httpContext?.TraceIdentifier;
if (traceId != null)
{
problemDetails.Extensions["traceId"] = traceId;
}
}
private string MakeFirstLetterLowercase(string str)
{
if (!string.IsNullOrEmpty(str) && char.IsUpper(str[0]))
{
return str.Length == 1 ? char.ToLower(str[0]).ToString() : char.ToLower(str[0]) + str[1..];
}
return str;
}
}
2 - In the Startup.cs override the default ProblemDetailsFactory:
services.AddSingleton<ProblemDetailsFactory, CustomProblemDetailsFactory>();
After that all keys in the dictionary 'errors' will start with lowercase
I'm trying to change the default value to Debit Memo when adding a new row in the Payments and Applications screen of the Accounts Receivable module. I've tried setting PXDefault(ARDocType.DebitMemo), but it doesn't appear to be working. Can anyone point me in the right direction?
The payments and applications page uses some interesting logic to determine the default value used, they define it in a call during the rowselected event for the header document.
protected virtual void ARPayment_RowSelected(PXCache cache, PXRowSelectedEventArgs e)
{
.....
SetDocTypeList(e.Row);
.....
}
public static void SetDocTypeList(PXCache cache, string docType)
{
string defValue = ARDocType.Invoice;
List<string> values = new List<string>();
List<string> labels = new List<string>();
if (docType == ARDocType.Refund)
{
defValue = ARDocType.CreditMemo;
values.AddRange(new string[] { ARDocType.CreditMemo, ARDocType.Payment, ARDocType.Prepayment });
labels.AddRange(new string[] { Messages.CreditMemo, Messages.Payment, Messages.Prepayment });
}
else if (docType == ARDocType.Payment || docType == ARDocType.VoidPayment)
{
values.AddRange(new string[] { ARDocType.Invoice, ARDocType.DebitMemo, ARDocType.CreditMemo, ARDocType.FinCharge });
labels.AddRange(new string[] { Messages.Invoice, Messages.DebitMemo, Messages.CreditMemo, Messages.FinCharge });
}
else
{
values.AddRange(new string[] { ARDocType.Invoice, ARDocType.DebitMemo, ARDocType.FinCharge });
labels.AddRange(new string[] { Messages.Invoice, Messages.DebitMemo, Messages.FinCharge });
}
if (!PXAccess.FeatureInstalled<FeaturesSet.overdueFinCharges>() && values.Contains(ARDocType.FinCharge) && labels.Contains(Messages.FinCharge))
{
values.Remove(ARDocType.FinCharge);
labels.Remove(Messages.FinCharge);
}
PXDefaultAttribute.SetDefault<ARAdjust.adjdDocType>(cache, defValue);
PXStringListAttribute.SetList<ARAdjust.adjdDocType>(cache, null, values.ToArray(), labels.ToArray());
}
private void SetDocTypeList(object Row)
{
ARPayment row = Row as ARPayment;
if (row != null)
{
SetDocTypeList(Adjustments.Cache, row.DocType);
}
}
To obtain the default you require you may implement the following code :
public class ARPaymentEntryExtension : PXGraphExtension<ARPaymentEntry>
{
protected virtual void ARPayment_RowSelected(PXCache cache, PXRowSelectedEventArgs e)
{
PXDefaultAttribute.SetDefault<ARAdjust.adjdDocType>(Base.Adjustments.Cache, ARDocType.DebitMemo);
}
}
I tried this solution: Custom fields with FormBuilder in the Microsoft Bot Framework
But failed to get it working....The problem I encountered is that when I assign the base.Form = value, the _prompt in the _field gets a default recognizer, and it won't get overriden in the next line's SetRecognizer call, that only replaces the _field's recognizer.
However the matching process uses the _prompt's recognizer internally ( ? ).
Here is my code:
public class LuisIntentRecognizer<T> : RecognizePrimitive<T>
where T : class
{
public LuisIntentRecognizer(IField<T> field, string luisModelID, string luisSubscriptionKey)
: base(field)
{
_luisModelID = luisModelID;
_luisSubscriptionKey = luisSubscriptionKey;
}
public override DescribeAttribute ValueDescription(object value)
{
return new DescribeAttribute((string)value);
}
public override IEnumerable<string> ValidInputs(object value)
{
yield return (string)value;
}
public override TermMatch Parse(string input)
{
TermMatch result = null;
if (!string.IsNullOrWhiteSpace(input))
{
var luisModel = new LuisModelAttribute(_luisModelID, _luisSubscriptionKey);
var luisService = new LuisService(luisModel);
var luisResult = luisService.QueryAsync(input).Result; // TODO refactor somehow to async
var winner = luisResult.Intents.MaxBy(i => i.Score ?? 0d);
if (winner != null && !string.IsNullOrEmpty(winner.Intent))
{
result = new TermMatch(0, winner.Intent.Length, 0.0, winner.Intent);
}
else
{
result = new TermMatch(0, input.Length, 0.0, input);
}
}
return result;
}
public override string Help(T state, object defaultValue)
{
var prompt = new Prompter<T>(_field.Template(TemplateUsage.StringHelp), _field.Form, null);
var args = HelpArgs(state, defaultValue);
return prompt.Prompt(state, _field.Name, args.ToArray()).Prompt;
}
private string _luisModelID;
private string _luisSubscriptionKey;
}
public class LuisIntentField<T> : FieldReflector<T>
where T : class
{
public LuisIntentField(string name, string luisModelID, string luisSubscriptionKey, bool ignoreAnnotations = false)
: base(name, ignoreAnnotations)
{
_luisModelID = luisModelID;
_luisSubscriptionKey = luisSubscriptionKey;
}
public override IForm<T> Form
{
set
{
base.Form = value;
base.SetRecognizer(new LuisIntentRecognizer<T>(this, _luisModelID, _luisSubscriptionKey));
}
}
private string _luisModelID;
private string _luisSubscriptionKey;
}
Could anyone get it working?
Thanks
It seems to be a bug in the framework indeed: https://github.com/Microsoft/BotBuilder/issues/879
i have a String like "Hello [everyone]!". "Everyone" should be replaced with a commandLink that points to an Page-Object.
For Example:
My Code detects "[everyone]" and creates a new "Page" with the headline "everyone" in my JavaDB Database. Now i want that [everyone] will shown as an commandLink:
Hello <h:commandLink value="everyone" action="#{PageController.getPage(everyone)}" />
or something else.
ATM i have this code to display the text with the []-Tags:
<h:outputText value="#{PageController.currentPage.latestContent.text}" />
Now what is the best practise to replace Tags (i.e. [XYZ]) with a specific commandLink? Or rather: how i can replace substrings with JSF-Tags (they should be rendered)? I have only found the possibility to create converter, but only examples to convert the complete string. :/ To find out the right substring i use Regulary Expressions.
Merry Christmas :)
My Soluion:
#ApplicationScoped
#Named("TagViewPhase")
public class TagViewPhase implements Serializable {
Application app;
public void beforePhase(PhaseEvent event) {
if (app == null) {
app = event.getFacesContext().getApplication();
}
if (event.getPhaseId() == PhaseId.RENDER_RESPONSE) {
scanForPageContent(event.getFacesContext().getViewRoot());
}
}
private void scanForPageContent(UIComponent component) {
for (UIComponent i : component.getChildren()) {
if ("pageContent".equals(i.getId())) {
HtmlOutputText content = (HtmlOutputText) i;
HtmlPanelGroup group = generatePageContent(content);
content.getParent().getChildren().add(group);
content.getParent().getChildren().remove(i);
} else {
scanForPageContent(i);
}
}
}
private HtmlPanelGroup generatePageContent(final HtmlOutputText pContent) {
List<UIComponent> childTree = new ArrayList<>();
String content = pContent.getValue().toString();
Pattern pattern = Pattern.compile("\\[(.*?)\\]");
Matcher matcher = pattern.matcher(content);
Integer start, end = 0;
String linkValue;
while (matcher.find()) {
start = matcher.start();
if (end < start) {
HtmlOutputText htmlText = (HtmlOutputText) app.createComponent(HtmlOutputText.COMPONENT_TYPE);
htmlText.setValue(content.substring(end, start));
childTree.add(htmlText);
}
end = matcher.end();
linkValue = content.substring(start + 1, end - 1);
HtmlCommandLink link = (HtmlCommandLink) app.createComponent(HtmlCommandLink.COMPONENT_TYPE);
link.setValue(linkValue);
link.setActionExpression(JSFExpression.createMethodExpression(
"#{PageController.switchPageByString('" + linkValue + "')}",
String.class,
new Class<?>[]{}
));
childTree.add(link);
}
if (end < content.length()) {
HtmlOutputText htmlText = (HtmlOutputText) app.createComponent(HtmlOutputText.COMPONENT_TYPE);
htmlText.setValue(content.substring(end, content.length()));
childTree.add(htmlText);
}
HtmlPanelGroup group = (HtmlPanelGroup) app.createComponent(HtmlPanelGroup.COMPONENT_TYPE);
group.getChildren().addAll(childTree);
return group;
}
}
xhtml File:
<f:view beforePhase="#{TagViewPhase.beforePhase}">
<h:panelGroup>
<h:outputText id="pageContent" value="#{PageController.currentPageContent.text}" />
</h:panelGroup>
</f:view>
Trying to save selections from a CheckBoxList as a comma-separated list (string) in DB (one or more choices selected). I am using a proxy in order to save as a string because otherwise I'd have to create separate tables in the DB for a relation - the work is not worth it for this simple scenario and I was hoping that I could just convert it to a string and avoid that.
The CheckBoxList uses an enum for it's choices:
public enum Selection
{
Selection1,
Selection2,
Selection3
}
Not to be convoluted, but I use [Display(Name="Choice 1")] and an extension class to display something friendly on the UI. Not sure if I can save that string instead of just the enum, although I think if I save as enum it's not a big deal for me to "display" the friendly string on UI on some confirmation page.
This is the "Record" class that saves a string in the DB:
public virtual string MyCheckBox { get; set; }
This is the "Proxy", which is some sample I found but not directly dealing with enum, and which uses IEnumerable<string> (or should it be IEnumerable<Selection>?):
public IEnumerable<string> MyCheckBox
{
get
{
if (String.IsNullOrWhiteSpace(Record.MyCheckBox)) return new string[] { };
return Record
.MyCheckBox
.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
.Select(r => r.Trim())
.Where(r => !String.IsNullOrEmpty(r));
}
set
{
Record.MyCheckBox = value == null ? null : String.Join(",", value);
}
}
To save in the DB, I am trying to do this in a create class:
proxy.MyCheckBox = record.MyCheckBox; //getting error here
but am getting the error:
Cannot implicitly convert 'string' to System.Collections.Generic.IEnumerable'
I don't know, if it's possible or better, to use Parse or ToString from the API for enum values.
I know that doing something like this will store whatever I put in the ("") into the DB, so it's just a matter of figuring out how to overcome the error (or, if there is an alternative):
proxy.MyCheckBox = new[] {"foo", "bar"};
I am not good with this stuff and have just been digging and digging to come up with a solution. Any help is much appreciated.
You can accomplish this using a custom user type. The example below uses an ISet<string> on the class and stores the values as a delimited string.
[Serializable]
public class CommaDelimitedSet : IUserType
{
const string delimiter = ",";
#region IUserType Members
public new bool Equals(object x, object y)
{
if (ReferenceEquals(x, y))
{
return true;
}
var xSet = x as ISet<string>;
var ySet = y as ISet<string>;
if (xSet == null || ySet == null)
{
return false;
}
// compare set contents
return xSet.Except(ySet).Count() == 0 && ySet.Except(xSet).Count() == 0;
}
public int GetHashCode(object x)
{
return x.GetHashCode();
}
public object NullSafeGet(IDataReader rs, string[] names, object owner)
{
var outValue = NHibernateUtil.String.NullSafeGet(rs, names[0]) as string;
if (string.IsNullOrEmpty(outValue))
{
return new HashSet<string>();
}
else
{
var splitArray = outValue.Split(new[] {Delimiter}, StringSplitOptions.RemoveEmptyEntries);
return new HashSet<string>(splitArray);
}
}
public void NullSafeSet(IDbCommand cmd, object value, int index)
{
var inValue = value as ISet<string>;
object setValue = inValue == null ? null : string.Join(Delimiter, inValue);
NHibernateUtil.String.NullSafeSet(cmd, setValue, index);
}
public object DeepCopy(object value)
{
// return new ISet so that Equals can work
// see http://www.mail-archive.com/nhusers#googlegroups.com/msg11054.html
var set = value as ISet<string>;
if (set == null)
{
return null;
}
return new HashSet<string>(set);
}
public object Replace(object original, object target, object owner)
{
return original;
}
public object Assemble(object cached, object owner)
{
return DeepCopy(cached);
}
public object Disassemble(object value)
{
return DeepCopy(value);
}
public SqlType[] SqlTypes
{
get { return new[] {new SqlType(DbType.String)}; }
}
public Type ReturnedType
{
get { return typeof(ISet<string>); }
}
public bool IsMutable
{
get { return false; }
}
#endregion
}
Usage in mapping file:
Map(x => x.CheckboxValues.CustomType<CommaDelimitedSet>();