Compare commits

..

No commits in common. "be914e4b2ed20924b0b1bbd0592dd6e48050d6bc" and "f62c1f267e04400f50b15b47f4cf2767feef48aa" have entirely different histories.

45 changed files with 4610 additions and 4948 deletions

View File

@ -1,226 +0,0 @@
# Remove the line below if you want to inherit .editorconfig settings from higher directories
root = true
# C# files
[*.cs]
#### Core EditorConfig Options ####
# Indentation and spacing
indent_size = 3
indent_style = tab
tab_width = 3
# New line preferences
end_of_line = crlf
insert_final_newline = false
#### .NET Coding Conventions ####
# Organize usings
dotnet_separate_import_directive_groups = false
dotnet_sort_system_directives_first = false
file_header_template = unset
# this. and Me. preferences
dotnet_style_qualification_for_event = false:suggestion
dotnet_style_qualification_for_field = false:suggestion
dotnet_style_qualification_for_method = false:suggestion
dotnet_style_qualification_for_property = false:suggestion
# Language keywords vs BCL types preferences
dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion
dotnet_style_predefined_type_for_member_access = true:suggestion
# Parentheses preferences
dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:suggestion
dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:suggestion
dotnet_style_parentheses_in_other_operators = never_if_unnecessary:suggestion
dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:suggestion
# Modifier preferences
dotnet_style_require_accessibility_modifiers = for_non_interface_members
# Expression-level preferences
dotnet_style_coalesce_expression = true
dotnet_style_collection_initializer = true
dotnet_style_explicit_tuple_names = true
dotnet_style_namespace_match_folder = true
dotnet_style_null_propagation = true
dotnet_style_object_initializer = true
dotnet_style_operator_placement_when_wrapping = beginning_of_line
dotnet_style_prefer_auto_properties = true:suggestion
dotnet_style_prefer_compound_assignment = true
dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion
dotnet_style_prefer_conditional_expression_over_return = true:suggestion
dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed
dotnet_style_prefer_inferred_anonymous_type_member_names = true
dotnet_style_prefer_inferred_tuple_names = false
dotnet_style_prefer_is_null_check_over_reference_equality_method = true
dotnet_style_prefer_simplified_boolean_expressions = true
dotnet_style_prefer_simplified_interpolation = true
# Field preferences
dotnet_style_readonly_field = true
# Parameter preferences
dotnet_code_quality_unused_parameters = all
# Suppression preferences
dotnet_remove_unnecessary_suppression_exclusions = 0
# New line preferences
dotnet_style_allow_multiple_blank_lines_experimental = false:suggestion
dotnet_style_allow_statement_immediately_after_block_experimental = false:suggestion
#### C# Coding Conventions ####
# var preferences
csharp_style_var_elsewhere = true:warning
csharp_style_var_for_built_in_types = true:warning
csharp_style_var_when_type_is_apparent = true:warning
# Expression-bodied members
csharp_style_expression_bodied_accessors = when_on_single_line:suggestion
csharp_style_expression_bodied_constructors = when_on_single_line:suggestion
csharp_style_expression_bodied_indexers = when_on_single_line:suggestion
csharp_style_expression_bodied_lambdas = when_on_single_line:suggestion
csharp_style_expression_bodied_local_functions = when_on_single_line:suggestion
csharp_style_expression_bodied_methods = when_on_single_line:suggestion
csharp_style_expression_bodied_operators = when_on_single_line:suggestion
csharp_style_expression_bodied_properties = when_on_single_line:suggestion
# Pattern matching preferences
csharp_style_pattern_matching_over_as_with_null_check = true
csharp_style_pattern_matching_over_is_with_cast_check = true
csharp_style_prefer_extended_property_pattern = true
csharp_style_prefer_not_pattern = true
csharp_style_prefer_pattern_matching = true:suggestion
csharp_style_prefer_switch_expression = true
# Null-checking preferences
csharp_style_conditional_delegate_call = true
# Modifier preferences
csharp_prefer_static_local_function = true
csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async
csharp_style_prefer_readonly_struct = true
# Code-block preferences
csharp_prefer_braces = when_multiline:suggestion
csharp_prefer_simple_using_statement = true
csharp_style_namespace_declarations = file_scoped:warning
csharp_style_prefer_method_group_conversion = true:suggestion
csharp_style_prefer_top_level_statements = false:suggestion
# Expression-level preferences
csharp_prefer_simple_default_expression = true
csharp_style_deconstructed_variable_declaration = true
csharp_style_implicit_object_creation_when_type_is_apparent = true
csharp_style_inlined_variable_declaration = true
csharp_style_prefer_index_operator = true
csharp_style_prefer_local_over_anonymous_function = true
csharp_style_prefer_null_check_over_type_check = true
csharp_style_prefer_range_operator = true
csharp_style_prefer_tuple_swap = true
csharp_style_prefer_utf8_string_literals = true
csharp_style_throw_expression = true
csharp_style_unused_value_assignment_preference = discard_variable
csharp_style_unused_value_expression_statement_preference = discard_variable:suggestion
# 'using' directive preferences
csharp_using_directive_placement = outside_namespace:suggestion
# New line preferences
csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true:suggestion
csharp_style_allow_blank_lines_between_consecutive_braces_experimental = false:suggestion
csharp_style_allow_embedded_statements_on_same_line_experimental = false:suggestion
#### C# Formatting Rules ####
# New line preferences
csharp_new_line_before_catch = true
csharp_new_line_before_else = true
csharp_new_line_before_finally = true
csharp_new_line_before_members_in_anonymous_types = true
csharp_new_line_before_members_in_object_initializers = true
csharp_new_line_before_open_brace = all
csharp_new_line_between_query_expression_clauses = true
# Indentation preferences
csharp_indent_block_contents = true
csharp_indent_braces = false
csharp_indent_case_contents = true
csharp_indent_case_contents_when_block = true
csharp_indent_labels = no_change
csharp_indent_switch_labels = true
# Space preferences
csharp_space_after_cast = false
csharp_space_after_colon_in_inheritance_clause = true
csharp_space_after_comma = true
csharp_space_after_dot = false
csharp_space_after_keywords_in_control_flow_statements = true
csharp_space_after_semicolon_in_for_statement = true
csharp_space_around_binary_operators = before_and_after
csharp_space_around_declaration_statements = false
csharp_space_before_colon_in_inheritance_clause = true
csharp_space_before_comma = false
csharp_space_before_dot = false
csharp_space_before_open_square_brackets = false
csharp_space_before_semicolon_in_for_statement = false
csharp_space_between_empty_square_brackets = false
csharp_space_between_method_call_empty_parameter_list_parentheses = false
csharp_space_between_method_call_name_and_opening_parenthesis = false
csharp_space_between_method_call_parameter_list_parentheses = false
csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
csharp_space_between_method_declaration_name_and_open_parenthesis = false
csharp_space_between_method_declaration_parameter_list_parentheses = false
csharp_space_between_parentheses = false
csharp_space_between_square_brackets = false
# Wrapping preferences
csharp_preserve_single_line_blocks = true
csharp_preserve_single_line_statements = true
#### Naming styles ####
# Naming rules
dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion
dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface
dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i
dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.types_should_be_pascal_case.symbols = types
dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case
dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members
dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case
# Symbol specifications
dotnet_naming_symbols.interface.applicable_kinds = interface
dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.interface.required_modifiers =
dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum
dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.types.required_modifiers =
dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.non_field_members.required_modifiers =
# Naming styles
dotnet_naming_style.pascal_case.required_prefix =
dotnet_naming_style.pascal_case.required_suffix =
dotnet_naming_style.pascal_case.word_separator =
dotnet_naming_style.pascal_case.capitalization = pascal_case
dotnet_naming_style.begins_with_i.required_prefix = I
dotnet_naming_style.begins_with_i.required_suffix =
dotnet_naming_style.begins_with_i.word_separator =
dotnet_naming_style.begins_with_i.capitalization = pascal_case

View File

@ -5,14 +5,14 @@
<CascadingValue Name="SubscribeToParentForm" Value="false" IsFixed="true">
<div class="@AutocompleteClassList">
<InputControl Label="@Label" Variant="@Variant" HelperText="@HelperText" HelperTextOnFocus="@HelperTextOnFocus" FullWidth="@FullWidth" Margin="@Margin" Class="@ClassList()" Style="@Style"
Error="@HasError" ErrorText="@ErrorText" Disabled="@Disabled" @onclick="@ToggleMenu" Required="@Required" ForId="@FieldId">
Error="@Error" ErrorText="@ErrorText" Disabled="@Disabled" @onclick="@ToggleMenu" Required="@Required" ForId="@FieldId">
<InputContent>
<Input @ref="_elementReference" @key="_elementKey" InputType="InputType.Text"
Class="select-input" Margin="@Margin"
Variant="@Variant"
TextUpdateSuppression="@TextUpdateSuppression"
Value="@Text" DisableUnderLine="@DisableUnderLine"
Disabled="@Disabled" ReadOnly="@ReadOnly" Error="@HasError"
Disabled="@Disabled" ReadOnly="@ReadOnly" Error="@Error"
OnAdornmentClick="@OnAdornmentClick" AdornmentIcon="@CurrentIcon" Adornment="@Adornment" AdornmentColor="@AdornmentColor" IconSize="@IconSize" AdornmentText="@AdornmentText"
Clearable="@Clearable" OnClearButtonClick="@OnClearButtonClick"
@attributes="UserAttributes"

View File

@ -83,7 +83,10 @@ public partial class Autocomplete<T> : InputBase<T>, IDisposable
_toStringFunc = value;
SetConverter(new LambdaConverter<T, string>(_toStringFunc ?? (x => x?.ToString()), null));
Converter = new Converter<T>
{
SetFunc = _toStringFunc ?? (x => x?.ToString()),
};
}
}
/// <summary>
@ -458,7 +461,7 @@ public partial class Autocomplete<T> : InputBase<T>, IDisposable
return string.Empty;
try
{
return Converter.Convert(item);
return Converter.Set(item);
}
catch (NullReferenceException) { }
@ -625,7 +628,7 @@ public partial class Autocomplete<T> : InputBase<T>, IDisposable
_timer?.Dispose();
var value = Converter.ConvertBack(Text);
var value = Converter.Get(Text);
return SetValueAsync(value, updateText: false);
}

View File

@ -44,17 +44,17 @@ public class BooleanInput<T> : FormComponent<T, bool?>
/// </summary>
[Parameter] public EventCallback<T> CheckedChanged { get; set; }
protected bool? BoolValue => Converter.Convert(Checked);
protected bool? BoolValue => Converter.Set(Checked);
protected virtual Task OnChange(ChangeEventArgs args)
{
Modified = true;
Touched = true;
return SetBoolValueAsync((bool?)args.Value);
}
protected Task SetBoolValueAsync(bool? value)
{
return SetCheckedAsync(Converter.ConvertBack(value));
return SetCheckedAsync(Converter.Get(value));
}
protected async Task SetCheckedAsync(T value)
@ -74,7 +74,7 @@ public class BooleanInput<T> : FormComponent<T, bool?>
{
var changed = base.SetConverter(value);
if (changed)
SetBoolValueAsync(Converter.Convert(Checked)).AndForget();
SetBoolValueAsync(Converter.Set(Checked)).AndForget();
return changed;
}

View File

@ -139,7 +139,7 @@ public partial class CheckBox<T> : BooleanInput<T>
protected override Task OnChange(ChangeEventArgs args)
{
Modified = true;
Touched = true;
// Apply only when TriState parameter is set to true and T is bool?
if (TriState && typeof(T) == typeof(bool?))

View File

@ -116,7 +116,11 @@ public partial class DataGrid<T> : UIComponent
internal bool isEditFormOpen;
// converters
private Converter<bool, bool?> _oppositeBoolConverter = new LambdaConverter<bool, bool?>(value => value ? false : true, value => value.HasValue ? !value.Value : true);
private Converter<bool, bool?> _oppositeBoolConverter = new Converter<bool, bool?>
{
SetFunc = value => value ? false : true,
GetFunc = value => value.HasValue ? !value.Value : true,
};
#region Notify Children Delegates

View File

@ -30,7 +30,7 @@ public class DatePicker : DatePickerBase
{
if (_value != date)
{
Modified = true;
Touched = true;
if (date is not null && IsDateDisabledFunc(date.Value.Date))
{
@ -40,8 +40,10 @@ public class DatePicker : DatePickerBase
_value = date;
if (updateValue)
await SetTextAsync(Converter.Convert(_value), false);
{
Converter.GetError = false;
await SetTextAsync(Converter.Set(_value), false);
}
await DateChanged.InvokeAsync(_value);
BeginValidate();
FieldChanged(_value);
@ -50,15 +52,15 @@ public class DatePicker : DatePickerBase
protected override Task DateFormatChanged(string newFormat)
{
Modified = true;
return SetTextAsync(Converter.Convert(_value), false);
Touched = true;
return SetTextAsync(Converter.Set(_value), false);
}
protected override Task StringValueChanged(string value)
{
Modified = true;
Touched = true;
// Update the date property (without updating back the Value property)
return SetDateAsync(Converter.ConvertBack(value), false);
return SetDateAsync(Converter.Get(value), false);
}
protected override string GetDayClasses(int month, DateTime day)

View File

@ -230,7 +230,7 @@ public abstract partial class DatePickerBase : Picker<DateTime?>
base.OnPickerOpened();
if (Editable == true && Text != null)
{
DateTime? a = Converter.ConvertBack(Text);
DateTime? a = Converter.Get(Text);
if (a.HasValue)
{
a = new DateTime(a.Value.Year, a.Value.Month, 1);

View File

@ -1,7 +1,4 @@
using Connected.Extensions;
using Connected.Utilities.BindingConverters;
namespace Connected.Components;
namespace Connected.Components;
public class DateRange : Range<DateTime?>, IEquatable<DateRange>
{
@ -18,7 +15,7 @@ public class DateRange : Range<DateTime?>, IEquatable<DateRange>
if (Start == null || End == null)
return string.Empty;
return RangeConverter<DateTime>.Join(converter.Convert(Start.Value), converter.Convert(End.Value));
return RangeConverter<DateTime>.Join(converter.Set(Start.Value), converter.Set(End.Value));
}
public string ToIsoDateString()
@ -43,14 +40,12 @@ public class DateRange : Range<DateTime?>, IEquatable<DateRange>
{
date = null;
var endDate = converter.ConvertBack(end);
if (endDate is null)
var endDate = converter.Get(end);
if (converter.GetError)
return false;
var startDate = converter.ConvertBack(start);
if (startDate is null)
var startDate = converter.Get(start);
if (converter.GetError)
return false;
date = new DateRange(startDate, endDate);

View File

@ -6,13 +6,13 @@
@code{
protected override RenderFragment InputContent=>
@<InputControl Label="@Label" Variant="@Variant" HelperText="@HelperText" Error="@HasError"
@<InputControl Label="@Label" Variant="@Variant" HelperText="@HelperText" Error="@Error"
ErrorText="@ErrorText" Disabled="@Disabled" Margin="@Margin" Required="@Required"
@onclick="() => { if (!Editable) ToggleState(); }" ForId="@FieldId">
<InputContent>
<RangeInput @ref="_rangeInput" @attributes="UserAttributes" InputType="@InputType.Text" Class="@PickerInputClass" Style="@Style" Variant="@Variant" ReadOnly="@(!Editable)"
@bind-Value="@RangeText" Disabled="@Disabled" Adornment="@Adornment" AdornmentIcon="@AdornmentIcon" AdornmentColor="@AdornmentColor" IconSize="@IconSize" OnAdornmentClick="ToggleState"
Required="@Required" RequiredError="@RequiredError" Error="@HasError" ErrorText="@ErrorText" Margin="@Margin" AdornmentAriaLabel="@AdornmentAriaLabel"/>
Required="@Required" RequiredError="@RequiredError" Error="@Error" ErrorText="@ErrorText" Margin="@Margin" AdornmentAriaLabel="@AdornmentAriaLabel"/>
</InputContent>
</InputControl>;

View File

@ -57,6 +57,7 @@ public partial class DateRangePicker : DatePickerBase
if (updateValue)
{
Converter.GetError = false;
if (_dateRange == null)
{
_rangeText = null;
@ -65,8 +66,8 @@ public partial class DateRangePicker : DatePickerBase
else
{
_rangeText = new Range<string>(
Converter.Convert(_dateRange.Start),
Converter.Convert(_dateRange.End));
Converter.Set(_dateRange.Start),
Converter.Set(_dateRange.End));
await SetTextAsync(_dateRange.ToString(Converter), false);
}
}
@ -84,7 +85,7 @@ public partial class DateRangePicker : DatePickerBase
if (_rangeText?.Equals(value) ?? value == null)
return;
Modified = true;
Touched = true;
_rangeText = value;
SetDateRangeAsync(ParseDateRangeValue(value?.Start, value?.End), false).AndForget();
}
@ -134,13 +135,13 @@ public partial class DateRangePicker : DatePickerBase
protected override Task DateFormatChanged(string newFormat)
{
Modified = true;
Touched = true;
return SetTextAsync(_dateRange?.ToString(Converter), false);
}
protected override Task StringValueChanged(string value)
{
Modified = true;
Touched = true;
// Update the daterange property (without updating back the Value property)
return SetDateRangeAsync(ParseDateRangeValue(value), false);
}

View File

@ -109,14 +109,14 @@ public partial class FileUpload<T> : FormComponent<T, string>
await FilesChanged.InvokeAsync(_value);
BeginValidate();
FieldChanged(_value);
if (!HasError || !SuppressOnChangeWhenInvalid) //only trigger FilesChanged if validation passes or SuppressOnChangeWhenInvalid is false
if (!Error || !SuppressOnChangeWhenInvalid) //only trigger FilesChanged if validation passes or SuppressOnChangeWhenInvalid is false
await OnFilesChanged.InvokeAsync(args);
}
protected override void OnInitialized()
{
//if (!(typeof(T) == typeof(IReadOnlyList<IBrowserFile>) || typeof(T) == typeof(IBrowserFile)))
// Logger.LogWarning("T must be of type {type1} or {type2}", typeof(IReadOnlyList<IBrowserFile>), typeof(IBrowserFile));
if (!(typeof(T) == typeof(IReadOnlyList<IBrowserFile>) || typeof(T) == typeof(IBrowserFile)))
Logger.LogWarning("T must be of type {type1} or {type2}", typeof(IReadOnlyList<IBrowserFile>), typeof(IBrowserFile));
base.OnInitialized();
}

View File

@ -206,11 +206,11 @@ public partial class Form : UIComponent, IDisposable, IForm
// - none have an error
// - all required fields have been touched (and thus validated)
var no_errors = _formControls.All(x => x.HasErrors == false);
var required_all_touched = _formControls.Where(x => x.Required).All(x => x.Modified);
var required_all_touched = _formControls.Where(x => x.Required).All(x => x.Touched);
var valid = no_errors && required_all_touched;
var old_touched = _touched;
_touched = _formControls.Any(x => x.Modified);
_touched = _formControls.Any(x => x.Touched);
try
{
_shouldRender = false;

View File

@ -14,19 +14,13 @@ public abstract class FormComponent<T, U> : UIComponent, IFormComponent, IDispos
{
private Converter<T, U> _converter;
/// <summary>
/// Invoked whenever the string value cannot be converted
/// </summary>
public event EventHandler<string> ConversionErrorOccured;
protected FormComponent(Converter<T, U> converter)
{
_converter = converter ?? throw new ArgumentNullException(nameof(converter));
_converter.ErrorOccured += (s, e) => OnConversionError(e);
_converter.OnError = OnConversionError;
}
[CascadingParameter]
internal IForm? Form { get; set; }
[CascadingParameter] internal IForm Form { get; set; }
/// <summary>
/// If true, this is a top-level form component. If false, this input is a sub-component of another input (i.e. TextField, Select, etc).
@ -39,72 +33,77 @@ public abstract class FormComponent<T, U> : UIComponent, IFormComponent, IDispos
/// If true, this form input is required to be filled out.
/// </summary>
[Parameter]
[Category(CategoryTypes.FormComponent.Validation)]
public bool Required { get; set; }
/// <summary>
/// The error text that will be displayed if the input is not filled out but required.
/// </summary>
[Parameter]
[Category(CategoryTypes.FormComponent.Validation)]
public string RequiredError { get; set; } = "Required";
/// <summary>
/// The ErrorText that will be displayed if <see cref="HasError"/> is set to true.
/// The ErrorText that will be displayed if Error true.
/// </summary>
[Parameter]
[Category(CategoryTypes.FormComponent.Validation)]
public string ErrorText { get; set; }
/// <summary>
/// If true, the label will be displayed in an error state.
/// </summary>
[Parameter]
public bool HasError { get; set; }
[Category(CategoryTypes.FormComponent.Validation)]
public bool Error { get; set; }
/// <summary>
/// The ErrorId that will be used by aria-describedby if <see cref="HasError"/> is true
/// The ErrorId that will be used by aria-describedby if Error true
/// </summary>
[Parameter]
[Category(CategoryTypes.FormComponent.Validation)]
public string ErrorId { get; set; }
/// <summary>
/// The generic converter of the component.
/// </summary>
public Converter<T, U> Converter => _converter;
[Parameter]
[Category(CategoryTypes.FormComponent.Behavior)]
public Converter<T, U> Converter
{
get => _converter;
set => SetConverter(value);
}
protected virtual bool SetConverter(Converter<T, U> value)
{
var changed = (_converter != value);
if (changed)
{
_converter = value ?? throw new ArgumentNullException(nameof(value)); // converter is mandatory at all times
_converter.OnError = OnConversionError;
}
return changed;
}
/// <summary>
/// The culture of the component. Also sets the culture of the <see cref="Converter"/> .
/// The culture of the component.
/// </summary>
[Parameter]
[Category(CategoryTypes.FormComponent.Behavior)]
public CultureInfo Culture
{
get => _converter.Culture;
set => SetCulture(value);
}
private string _conversionError { get; set; }
protected virtual bool SetConverter(Converter<T, U> value)
{
var changed = _converter != value;
if (changed)
{
/*
* Converter is mandatory at all times
*/
_converter = value ?? throw new ArgumentNullException(nameof(value));
_converter.ErrorOccured += (s, e) => OnConversionError(e);
}
return changed;
}
protected virtual bool SetCulture(CultureInfo value)
{
var changed = _converter.Culture != value;
var changed = (_converter.Culture != value);
if (changed)
{
_converter.Culture = value;
}
return changed;
}
@ -112,38 +111,37 @@ public abstract class FormComponent<T, U> : UIComponent, IFormComponent, IDispos
{
// note: we need to update the form here because the conversion error might lead to not updating the value
// ... which leads to not updating the form
//TODO Why does the form need to be updated?
Modified = true;
_conversionError = error;
Touched = true;
Form?.Update(this);
OnConversionErrorOccurred(error);
}
ConversionErrorOccured?.Invoke(this, error);
protected virtual void OnConversionErrorOccurred(string error)
{
/* Descendants can override this method to catch conversion errors */
}
/// <summary>
/// True if the conversion from string to T failed
/// </summary>
public bool ConversionError => !string.IsNullOrWhiteSpace(_conversionError);
public bool ConversionError => _converter.GetError;
/// <summary>
/// The error message of the conversion error from string to T. Null otherwise
/// </summary>
public string ConversionErrorMessage => _conversionError;
public string ConversionErrorMessage => _converter.GetErrorMessage;
/// <summary>
/// True if the input has any of the following errors: An error set from outside, a conversion error or
/// one or more validation errors
/// </summary>
public bool HasErrors => HasError || ConversionError || ValidationErrors.Count > 0;
public bool HasErrors => Error || ConversionError || ValidationErrors.Count > 0;
/// <summary>
/// Return the validation error text or the conversion error message.
/// </summary>
/// <returns>Error text/message</returns>
public string? GetErrorText()
public string GetErrorText()
{
// ErrorText is either set from outside or the first validation error
if (!IsNullOrWhiteSpace(ErrorText))
@ -156,13 +154,13 @@ public abstract class FormComponent<T, U> : UIComponent, IFormComponent, IDispos
}
/// <summary>
/// This manages the state of having been modified by the user. A form control always starts out unmodified
/// but becomes modified when the user performed input or the blur event was raised.
/// This manages the state of having been "touched" by the user. A form control always starts out untouched
/// but becomes touched when the user performed input or the blur event was raised.
///
/// The modified state is only relevant for inputs that have no value (i.e. empty text fields). Being unmodified will
/// suppress the display of the <see cref="RequiredError"/>
/// The touched state is only relevant for inputs that have no value (i.e. empty text fields). Being untouched will
/// suppress RequiredError
/// </summary>
public bool Modified { get; protected set; }
public bool Touched { get; protected set; }
#region MudForm Validation
@ -184,20 +182,10 @@ public abstract class FormComponent<T, U> : UIComponent, IFormComponent, IDispos
[Category(CategoryTypes.FormComponent.Validation)]
public object Validation { get; set; }
private T __value;
/// <summary>
/// This is the form component's value.
/// </summary>
protected T _value
{
get => __value;
set
{
__value = value;
_conversionError = null;
}
}
protected T _value;
// These are the fire-and-forget methods to launch an async validation process.
// After each async step, we make sure the current Value of the component has not changed while
@ -244,7 +232,7 @@ public abstract class FormComponent<T, U> : UIComponent, IFormComponent, IDispos
// when a validation is forced, we must set Touched to true, because for untouched fields with
// no value, validation does nothing due to the way forms are expected to work (display errors
// only after fields have been touched).
Modified = true;
Touched = true;
return ValidateValue();
}
@ -296,7 +284,7 @@ public abstract class FormComponent<T, U> : UIComponent, IFormComponent, IDispos
// required error (must be last, because it is least important!)
if (Required)
{
if (Modified && !HasValue(_value))
if (Touched && !HasValue(_value))
errors.Add(RequiredError);
}
}
@ -309,7 +297,7 @@ public abstract class FormComponent<T, U> : UIComponent, IFormComponent, IDispos
// if Error and ErrorText are set by the user, setting them here will have no effect.
// if Error, create an error id that can be used by aria-describedby on input control
ValidationErrors = errors;
HasError = errors.Count > 0;
Error = errors.Count > 0;
ErrorText = errors.FirstOrDefault();
ErrorId = HasErrors ? Guid.NewGuid().ToString() : null;
Form?.Update(this);
@ -499,8 +487,7 @@ public abstract class FormComponent<T, U> : UIComponent, IFormComponent, IDispos
{
/* to be overridden */
_value = default;
_conversionError = null;
Modified = false;
Touched = false;
StateHasChanged();
}
@ -509,7 +496,7 @@ public abstract class FormComponent<T, U> : UIComponent, IFormComponent, IDispos
/// </summary>
public void ResetValidation()
{
HasError = false;
Error = false;
ValidationErrors.Clear();
ErrorText = null;
StateHasChanged();
@ -540,26 +527,28 @@ public abstract class FormComponent<T, U> : UIComponent, IFormComponent, IDispos
/// <summary>
/// Specify an expression which returns the model's field for which validation messages should be displayed.
/// </summary>
#nullable enable
[Parameter]
[Category(CategoryTypes.FormComponent.Validation)]
public Expression<Func<T>>? For { get; set; }
#nullable disable
public bool IsForNull => For == null;
/// <summary>
/// Stores the list of validation attributes attached to the property targeted by <seealso cref="For"/>. If <seealso cref="For"/> is null, this property is null too.
/// </summary>
#nullable enable
private IEnumerable<ValidationAttribute>? _validationAttrsFor;
#nullable disable
private void OnValidationStateChanged(object sender, ValidationStateChangedEventArgs e)
{
if (EditContext != null && !_fieldIdentifier.Equals(default(FieldIdentifier)))
{
var error_msgs = EditContext.GetValidationMessages(_fieldIdentifier).ToArray();
HasError = error_msgs.Length > 0;
ErrorText = (HasError ? error_msgs[0] : null);
Error = error_msgs.Length > 0;
ErrorText = (Error ? error_msgs[0] : null);
StateHasChanged();
}
}
@ -572,13 +561,16 @@ public abstract class FormComponent<T, U> : UIComponent, IFormComponent, IDispos
/// <summary>
/// To find out whether or not For parameter has changed we keep a separate reference
/// </summary>
#nullable enable
private Expression<Func<T>>? _currentFor;
#nullable disable
/// <summary>
/// To find out whether or not EditContext parameter has changed we keep a separate reference
/// </summary>
#nullable enable
private EditContext? _currentEditContext;
#nullable disable
protected override void OnParametersSet()
{

View File

@ -3,9 +3,9 @@
public interface IFormComponent
{
public bool Required { get; set; }
public bool HasError { get; set; }
public bool Error { get; set; }
public bool HasErrors { get; }
public bool Modified { get; }
public bool Touched { get; }
public object Validation { get; set; }
public bool IsForNull { get; }
public List<string> ValidationErrors { get; set; }

View File

@ -1,6 +1,5 @@
using System.Timers;
using Connected.Annotations;
using Connected.Utilities.BindingConverters;
using Microsoft.AspNetCore.Components;
namespace Connected.Components;

View File

@ -41,7 +41,7 @@
@onkeyup:preventDefault="@KeyUpPreventDefault"
@onmousewheel="@OnMouseWheel"
@onwheel="@OnMouseWheel"
aria-invalid="@HasError.ToString().ToLower()"
aria-invalid="@Error.ToString().ToLower()"
aria-describedby="@ErrorId"
>
@Text
@ -73,7 +73,7 @@
@onkeyup:preventDefault="@KeyUpPreventDefault"
@onmousewheel="@OnMouseWheel"
@onwheel="@OnMouseWheel"
aria-invalid="@HasError.ToString().ToLower()"
aria-invalid="@Error.ToString().ToLower()"
aria-describedby="@ErrorId"
/>
@*Note: double mouse wheel handlers needed for Firefox because it doesn't know onmousewheel*@

View File

@ -215,7 +215,7 @@ public abstract class InputBase<T> : FormComponent<T, string>
{
Text = text;
if (!string.IsNullOrWhiteSpace(Text))
Modified = true;
Touched = true;
if (updateValue)
await UpdateValuePropertyAsync(false);
await TextChanged.InvokeAsync(Text);
@ -227,7 +227,7 @@ public abstract class InputBase<T> : FormComponent<T, string>
/// </summary>
protected virtual Task UpdateTextPropertyAsync(bool updateValue)
{
return SetTextAsync(Converter.Convert(Value), updateValue);
return SetTextAsync(Converter.Set(Value), updateValue);
}
/// <summary>
@ -266,7 +266,7 @@ public abstract class InputBase<T> : FormComponent<T, string>
if (!OnlyValidateIfDirty || _isDirty)
{
Modified = true;
Touched = true;
BeginValidateAfter(OnBlur.InvokeAsync(obj));
}
}
@ -361,7 +361,7 @@ public abstract class InputBase<T> : FormComponent<T, string>
/// </summary>
protected virtual Task UpdateValuePropertyAsync(bool updateText)
{
return SetValueAsync(Converter.ConvertBack(Text), updateText);
return SetValueAsync(Converter.Get(Text), updateText);
}
protected override bool SetConverter(Converter<T, string> value)
@ -389,7 +389,7 @@ public abstract class InputBase<T> : FormComponent<T, string>
[Category(CategoryTypes.FormComponent.Behavior)]
public string Format
{
get => ((ToStringConverter<T>)Converter).Format;
get => ((Converter<T>)Converter).Format;
set => SetFormat(value);
}
@ -398,7 +398,7 @@ public abstract class InputBase<T> : FormComponent<T, string>
var changed = Format != value;
if (changed)
{
((ToStringConverter<T>)Converter).Format = value;
((Converter<T>)Converter).Format = value;
UpdateTextPropertyAsync(false).AndForget(); // refresh only Text property from current Value
}
return changed;

View File

@ -1,5 +1,4 @@
using Connected.Extensions;
using Connected.Utilities.BindingConverters;
using Microsoft.AspNetCore.Components;
namespace Connected.Components;
@ -11,7 +10,7 @@ public partial class RangeInput<T> : InputBase<Range<T>>
public RangeInput()
{
Value = new Range<T>();
SetConverter(new RangeConverter<T>());
Converter = new RangeConverter<T>();
}
protected string Classname => InputCssHelper.GetClassname(this,

View File

@ -238,7 +238,7 @@ public partial class Mask : InputBase<string>, IDisposable
await base.SetTextAsync(text, updateValue: false);
if (Clearable)
UpdateClearable(Text);
var v = Converter.ConvertBack(cleanText);
var v = Converter.Get(cleanText);
Value = v;
await ValueChanged.InvokeAsync(v);
SetCaretPosition(caret, selection);
@ -262,7 +262,7 @@ public partial class Mask : InputBase<string>, IDisposable
// allow this only via changes from the outside
if (_updating)
return;
var text = Converter.Convert(Value);
var text = Converter.Set(Value);
var cleanText = MaskKind.GetCleanText();
if (cleanText == text || string.IsNullOrEmpty(cleanText) && string.IsNullOrEmpty(text))
return;

View File

@ -39,7 +39,7 @@
AdornmentIcon="@AdornmentIcon"
AdornmentColor="@AdornmentColor"
IconSize="@IconSize"
Error="@HasError"
Error="@Error"
Immediate="@(Immediate)"
Margin="@Margin"
MaxLength="@MaxLength"

View File

@ -8,7 +8,6 @@ using System.Globalization;
using Connected.Annotations;
using Connected.Services;
using Connected.Utilities;
using Connected.Utilities.BindingConverters;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
@ -209,7 +208,7 @@ public partial class NumericField<T> : DebouncedInput<T>
return (T)(object)Convert.ToInt64(FromInt64(Value) + FromInt64(Step) * factor);
if (typeof(T) == typeof(ulong) || typeof(T) == typeof(ulong?))
return (T)(object)Convert.ToUInt64(FromUInt64(Value) + FromUInt64(Step) * factor);
return Number.To<T>(Number.From(Value) + Number.From(Step) * factor);
return Num.To<T>(Num.From(Value) + Num.From(Step) * factor);
}
/// <summary>

View File

@ -31,7 +31,7 @@
Margin="@Margin"
Required="@Required"
RequiredError="@RequiredError"
Error="@HasError"
Error="@Error"
ErrorText="@ErrorText"
Clearable="@(ReadOnly ? false : Clearable)"
OnClearButtonClick="@(() => Clear())"

View File

@ -2,7 +2,6 @@
using Connected.Annotations;
using Connected.Extensions;
using Connected.Utilities;
using Connected.Utilities.BindingConverters;
using Microsoft.AspNetCore.Components;
namespace Connected.Components;

View File

@ -81,7 +81,7 @@ public partial class RadioGroup<T> : FormComponent<T, T>, IRadioGroup
internal Task SetSelectedRadioAsync(Radio<T> radio)
{
Modified = true;
Touched = true;
return SetSelectedRadioAsync(radio, true);
}

View File

@ -5,14 +5,14 @@
<CascadingValue Name="SubscribeToParentForm" Value="false" IsFixed="true">
<div class="mud-select" id="@_elementId">
<InputControl Label="@Label" Variant="@Variant" HelperText="@HelperText" HelperTextOnFocus="@HelperTextOnFocus" FullWidth="@FullWidth" Margin="@Margin" Class="@Classname" Style="@Style"
Error="@HasError" ErrorText="@ErrorText" ErrorId="@ErrorId" Disabled="@Disabled" @onclick="@ToggleMenu" Required="@Required" ForId="@FieldId">
Error="@Error" ErrorText="@ErrorText" ErrorId="@ErrorId" Disabled="@Disabled" @onclick="@ToggleMenu" Required="@Required" ForId="@FieldId">
<InputContent>
<Input @ref="_elementReference" InputType="@(CanRenderValue || (Strict && !IsValueInList) ? InputType.Hidden : InputType.Text)"
Class="mud-select-input" Margin="@Margin" Placeholder="@Placeholder"
Variant="@Variant"
TextUpdateSuppression="false"
Value="@(Strict && !IsValueInList ? null : Text)" DisableUnderLine="@DisableUnderLine"
Disabled="@Disabled" ReadOnly="true" Error="@HasError" ErrorId="@ErrorId"
Disabled="@Disabled" ReadOnly="true" Error="@Error" ErrorId="@ErrorId"
OnAdornmentClick="@OnAdornmentClick" AdornmentIcon="@_currentIcon" Adornment="@Adornment"
AdornmentColor="@AdornmentColor" IconSize="@IconSize" AdornmentText="@AdornmentText"
Clearable="@Clearable" OnClearButtonClick="(async (e) => await SelectClearButtonClickHandlerAsync(e))"

View File

@ -86,12 +86,12 @@ public partial class Select<T> : InputBase<T>, ISelect, IShadowSelect
// find first item that starts with the letter
var currentItem = items.FirstOrDefault(x => x.ItemId == (string)_activeItemId);
if (currentItem != null &&
Converter.Convert(currentItem.Value)?.ToLowerInvariant().StartsWith(startChar) == true)
Converter.Set(currentItem.Value)?.ToLowerInvariant().StartsWith(startChar) == true)
{
// this will step through all items that start with the same letter if pressed multiple times
items = items.SkipWhile(x => x != currentItem).Skip(1);
}
items = items.Where(x => Converter.Convert(x.Value)?.ToLowerInvariant().StartsWith(startChar) == true);
items = items.Where(x => Converter.Set(x.Value)?.ToLowerInvariant().StartsWith(startChar) == true);
}
var item = items.FirstOrDefault();
if (item == null)
@ -244,13 +244,13 @@ public partial class Select<T> : InputBase<T>, ISelect, IShadowSelect
//Warning. Here the Converter was not set yet
if (MultiSelectionTextFunc != null)
{
SetCustomizedTextAsync(string.Join(Delimiter, SelectedValues.Select(x => Converter.Convert(x))),
selectedConvertedValues: SelectedValues.Select(x => Converter.Convert(x)).ToList(),
SetCustomizedTextAsync(string.Join(Delimiter, SelectedValues.Select(x => Converter.Set(x))),
selectedConvertedValues: SelectedValues.Select(x => Converter.Set(x)).ToList(),
multiSelectionTextFunc: MultiSelectionTextFunc).AndForget();
}
else
{
SetTextAsync(string.Join(Delimiter, SelectedValues.Select(x => Converter.Convert(x))), updateValue: false).AndForget();
SetTextAsync(string.Join(Delimiter, SelectedValues.Select(x => Converter.Set(x))), updateValue: false).AndForget();
}
}
SelectedValuesChanged.InvokeAsync(new HashSet<T>(SelectedValues, _comparer));
@ -293,7 +293,11 @@ public partial class Select<T> : InputBase<T>, ISelect, IShadowSelect
if (_toStringFunc == value)
return;
_toStringFunc = value;
SetConverter(new LambdaConverter<T, string>(_toStringFunc ?? (x => x?.ToString()), null));
Converter = new Converter<T>
{
SetFunc = _toStringFunc ?? (x => x?.ToString()),
//GetFunc = LookupValue,
};
}
}
@ -389,15 +393,15 @@ public partial class Select<T> : InputBase<T>, ISelect, IShadowSelect
if (MultiSelectionTextFunc != null)
{
return MultiSelection
? SetCustomizedTextAsync(string.Join(Delimiter, SelectedValues.Select(x => Converter.Convert(x))),
selectedConvertedValues: SelectedValues.Select(x => Converter.Convert(x)).ToList(),
? SetCustomizedTextAsync(string.Join(Delimiter, SelectedValues.Select(x => Converter.Set(x))),
selectedConvertedValues: SelectedValues.Select(x => Converter.Set(x)).ToList(),
multiSelectionTextFunc: MultiSelectionTextFunc)
: base.UpdateTextPropertyAsync(updateValue);
}
else
{
return MultiSelection
? SetTextAsync(string.Join(Delimiter, SelectedValues.Select(x => Converter.Convert(x))))
? SetTextAsync(string.Join(Delimiter, SelectedValues.Select(x => Converter.Set(x))))
: base.UpdateTextPropertyAsync(updateValue);
}
}
@ -564,13 +568,13 @@ public partial class Select<T> : InputBase<T>, ISelect, IShadowSelect
if (MultiSelectionTextFunc != null)
{
await SetCustomizedTextAsync(string.Join(Delimiter, SelectedValues.Select(x => Converter.Convert(x))),
selectedConvertedValues: SelectedValues.Select(x => Converter.Convert(x)).ToList(),
await SetCustomizedTextAsync(string.Join(Delimiter, SelectedValues.Select(x => Converter.Set(x))),
selectedConvertedValues: SelectedValues.Select(x => Converter.Set(x)).ToList(),
multiSelectionTextFunc: MultiSelectionTextFunc);
}
else
{
await SetTextAsync(string.Join(Delimiter, SelectedValues.Select(x => Converter.Convert(x))), updateValue: false);
await SetTextAsync(string.Join(Delimiter, SelectedValues.Select(x => Converter.Set(x))), updateValue: false);
}
UpdateSelectAllChecked();
@ -807,7 +811,7 @@ public partial class Select<T> : InputBase<T>, ISelect, IShadowSelect
{
multiSelectionText = text;
if (!string.IsNullOrWhiteSpace(multiSelectionText))
Modified = true;
Touched = true;
if (updateValue)
await UpdateValuePropertyAsync(false);
await TextChanged.InvokeAsync(multiSelectionText);
@ -1000,13 +1004,13 @@ public partial class Select<T> : InputBase<T>, ISelect, IShadowSelect
_selectedValues = new HashSet<T>(selectedValues, _comparer);
if (MultiSelectionTextFunc != null)
{
await SetCustomizedTextAsync(string.Join(Delimiter, SelectedValues.Select(x => Converter.Convert(x))),
selectedConvertedValues: SelectedValues.Select(x => Converter.Convert(x)).ToList(),
await SetCustomizedTextAsync(string.Join(Delimiter, SelectedValues.Select(x => Converter.Set(x))),
selectedConvertedValues: SelectedValues.Select(x => Converter.Set(x)).ToList(),
multiSelectionTextFunc: MultiSelectionTextFunc);
}
else
{
await SetTextAsync(string.Join(Delimiter, SelectedValues.Select(x => Converter.Convert(x))), updateValue: false);
await SetTextAsync(string.Join(Delimiter, SelectedValues.Select(x => Converter.Set(x))), updateValue: false);
}
UpdateSelectAllChecked();
_selectedValues = selectedValues; // need to force selected values because Blazor overwrites it under certain circumstances due to changes of Text or Value

View File

@ -129,7 +129,7 @@ public partial class SelectItem<T> : SelectItemBase, IDisposable
var converter = Select?.Converter;
if (converter == null)
return $"{Value}";
return converter.Convert(Value);
return converter.Set(Value);
}
}

View File

@ -28,8 +28,8 @@ public partial class Slider<T> : UIComponent
[Category(CategoryTypes.Slider.Validation)]
public T Min
{
get => Converter.ConvertBack(_min);
set => _min = Converter.Convert(value);
get => Converter.Get(_min);
set => _min = Converter.Set(value);
}
/// <summary>
@ -40,8 +40,8 @@ public partial class Slider<T> : UIComponent
[Category(CategoryTypes.Slider.Validation)]
public T Max
{
get => Converter.ConvertBack(_max);
set => _max = Converter.Convert(value);
get => Converter.Get(_max);
set => _max = Converter.Set(value);
}
/// <summary>
@ -52,8 +52,8 @@ public partial class Slider<T> : UIComponent
[Category(CategoryTypes.Slider.Validation)]
public T Step
{
get => Converter.ConvertBack(_step);
set => _step = Converter.Convert(value);
get => Converter.Get(_step);
set => _step = Converter.Set(value);
}
/// <summary>
@ -73,7 +73,7 @@ public partial class Slider<T> : UIComponent
[Parameter]
[Category(CategoryTypes.Slider.Behavior)]
public ToStringConverter<T> Converter { get; set; } = new DefaultConverter<T>() { Culture = CultureInfo.InvariantCulture };
public Converter<T> Converter { get; set; } = new DefaultConverter<T>() { Culture = CultureInfo.InvariantCulture };
[Parameter] public EventCallback<T> ValueChanged { get; set; }
@ -81,10 +81,10 @@ public partial class Slider<T> : UIComponent
[Category(CategoryTypes.Slider.Data)]
public T Value
{
get => Converter.ConvertBack(_value);
get => Converter.Get(_value);
set
{
var d = Converter.Convert(value);
var d = Converter.Set(value);
if (_value == d)
return;
_value = d;

View File

@ -9,7 +9,7 @@
HelperTextOnFocus="@HelperTextOnFocus"
CounterText="@GetCounterText()"
FullWidth="@FullWidth"
Class="@ClassList"
Class="@Classname"
Error="@HasErrors"
ErrorText="@GetErrorText()"
ErrorId="@ErrorId"
@ -43,7 +43,7 @@
AdornmentAriaLabel="@AdornmentAriaLabel"
IconSize="@IconSize"
OnAdornmentClick="@OnAdornmentClick"
Error="@HasError"
Error="@Error"
ErrorId="@ErrorId"
Immediate="@Immediate"
Margin="@Margin"
@ -83,7 +83,7 @@
AdornmentColor="@AdornmentColor"
IconSize="@IconSize"
OnAdornmentClick="@OnAdornmentClick"
Error="@HasError"
Error="@Error"
Immediate="@Immediate"
Margin="@Margin" OnBlur="@OnBlurred"
Clearable="@Clearable"

View File

@ -7,9 +7,13 @@ namespace Connected.Components;
public partial class TextField<T> : DebouncedInput<T>
{
private Mask? _maskReference;
protected string Classname =>
new CssBuilder("mud-input-input-control")
.AddClass(Class)
.Build();
public Input<string> InputReference { get; private set; }
private Mask _maskReference;
/// <summary>
/// Type of the input element. It should be a valid HTML5 input type.
@ -34,11 +38,6 @@ public partial class TextField<T> : DebouncedInput<T>
/// </summary>
[Parameter] public EventCallback<MouseEventArgs> OnClearButtonClick { get; set; }
protected string ClassList =>
new CssBuilder("mud-input-input-control")
.AddClass(Class)
.Build();
public override ValueTask FocusAsync()
{
if (_mask == null)
@ -131,10 +130,10 @@ public partial class TextField<T> : DebouncedInput<T>
{
if (_mask != null)
{
var textValue = Converter.Convert(value);
var textValue = Converter.Set(value);
_mask.SetText(textValue);
textValue = Mask.GetCleanText();
value = Converter.ConvertBack(textValue);
value = Converter.Get(textValue);
}
return base.SetValueAsync(value, updateText);
}

View File

@ -14,13 +14,11 @@ public partial class TimePicker : Picker<TimeSpan?>
private const string format24Hours = "HH:mm";
private const string format12Hours = "hh:mm tt";
public TimePicker() : base()
public TimePicker() : base(new DefaultConverter<TimeSpan?>())
{
_timeFormat = format24Hours;
SetConverter(new LambdaConverter<TimeSpan?, string>((e) => OnSet(e), (e) => OnGet(e)));
Converter.GetFunc = OnGet;
Converter.SetFunc = OnSet;
((DefaultConverter<TimeSpan?>)Converter).Format = format24Hours;
AdornmentIcon = Icons.Material.Filled.AccessTime;
AdornmentAriaLabel = "Open Time Picker";
}
@ -32,7 +30,7 @@ public partial class TimePicker : Picker<TimeSpan?>
var time = DateTime.Today.Add(timespan.Value);
return time.ToString(_timeFormat, Culture);
return time.ToString(((DefaultConverter<TimeSpan?>)Converter).Format, Culture);
}
private TimeSpan? OnGet(string value)
@ -40,7 +38,7 @@ public partial class TimePicker : Picker<TimeSpan?>
if (string.IsNullOrEmpty(value))
return null;
if (DateTime.TryParseExact(value, _timeFormat, Culture, DateTimeStyles.None, out var time))
if (DateTime.TryParseExact(value, ((DefaultConverter<TimeSpan?>)Converter).Format, Culture, DateTimeStyles.None, out var time))
{
return time.TimeOfDay;
}
@ -107,10 +105,13 @@ public partial class TimePicker : Picker<TimeSpan?>
_amPm = value;
_timeFormat = AmPm ? format12Hours : format24Hours;
if (Converter is DefaultConverter<TimeSpan?> defaultConverter && string.IsNullOrWhiteSpace(_timeFormat))
{
defaultConverter.Format = AmPm ? format12Hours : format24Hours;
}
Modified = true;
SetTextAsync(Converter.Convert(_value), false).AndForget();
Touched = true;
SetTextAsync(Converter.Set(_value), false).AndForget();
}
}
@ -128,9 +129,11 @@ public partial class TimePicker : Picker<TimeSpan?>
return;
_timeFormat = value;
if (Converter is DefaultConverter<TimeSpan?> defaultConverter)
defaultConverter.Format = _timeFormat;
Modified = true;
SetTextAsync(Converter.Convert(_value), false).AndForget();
Touched = true;
SetTextAsync(Converter.Set(_value), false).AndForget();
}
}
@ -152,7 +155,7 @@ public partial class TimePicker : Picker<TimeSpan?>
TimeIntermediate = time;
_value = time;
if (updateValue)
await SetTextAsync(Converter.Convert(_value), false);
await SetTextAsync(Converter.Set(_value), false);
UpdateTimeSetFromTime();
await TimeChanged.InvokeAsync(_value);
BeginValidate();
@ -167,9 +170,9 @@ public partial class TimePicker : Picker<TimeSpan?>
protected override Task StringValueChanged(string value)
{
Modified = true;
Touched = true;
// Update the time property (without updating back the Value property)
return SetTimeAsync(Converter.ConvertBack(value), false);
return SetTimeAsync(Converter.Get(value), false);
}
//The last line cannot be tested

View File

@ -12,7 +12,7 @@ public partial class TreeViewItem<T> : UIComponent
private string _text;
private bool _disabled;
private bool _isChecked, _isSelected, _isServerLoaded;
private ToStringConverter<T> _converter = new DefaultConverter<T>();
private Converter<T> _converter = new DefaultConverter<T>();
private readonly List<TreeViewItem<T>> _childItems = new();
protected string Classname =>
@ -68,7 +68,7 @@ public partial class TreeViewItem<T> : UIComponent
[Category(CategoryTypes.TreeView.Behavior)]
public string Text
{
get => string.IsNullOrEmpty(_text) ? _converter.Convert(Value) : _text;
get => string.IsNullOrEmpty(_text) ? _converter.Set(Value) : _text;
set => _text = value;
}

View File

@ -28,6 +28,10 @@
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="7.0.0-rc.2.22476.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Connected.Client\Connected.Client.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="Colors\" />
</ItemGroup>

View File

@ -1,48 +1,38 @@
using System.ComponentModel;
namespace Connected;
public enum InputType
namespace Connected
{
public enum InputType
{
[Description("text")]
Text,
[Description("password")]
Password,
[Description("email")]
Email,
[Description("hidden")]
Hidden,
[Description("number")]
Number,
[Description("search")]
Search,
[Description("tel")]
Telephone,
[Description("url")]
Url,
[Description("color")]
Color,
[Description("date")]
Date,
[Description("datetime-local")]
DateTimeLocal,
[Description("month")]
Month,
[Description("time")]
Time,
[Description("week")]
Week
}
}

63
Instance.cs Normal file
View File

@ -0,0 +1,63 @@
using Connected.Middleware;
using Connected.Startup;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.Extensions.DependencyInjection;
namespace Connected;
public static class Instance
{
public static WebAssemblyHost Host { get; private set; } = default!;
private static IServiceProvider ServiceProvider { get; set; } = default!;
internal static T? GetService<T>()
{
return ServiceProvider.GetService<T>();
}
public static async Task Start<TApp>(List<IStartup> startups, Dictionary<string, string> args) where TApp : IComponent
{
var builder = WebAssemblyHostBuilder.CreateDefault(UnpackArguments(args));
builder.RootComponents.Add<TApp>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");
builder.Services.AddHttpClient();
builder.Services.AddSingleton(typeof(IComponentMiddlewareService), typeof(ComponentMiddlewareService));
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
foreach (var startup in startups)
await startup.ConfigureServices(builder.Services);
Host = builder.Build();
foreach (var startup in startups)
await startup.Configure(Host);
await Host.RunAsync();
}
private static string[] UnpackArguments(Dictionary<string, string> args)
{
var result = new string[args.Count];
for (var i = 0; i < args.Count; i++)
{
var arg = args.ElementAt(i);
result[i] = string.IsNullOrWhiteSpace(arg.Value) ? arg.Key : $"{arg.Key}={arg.Value}";
}
return result;
}
public static void UseMiddlewareComponent<TMiddleware, TComponent>(this WebAssemblyHost app)
{
if (app.Services.GetService<IComponentMiddlewareService>() is not IComponentMiddlewareService service)
throw new NullReferenceException(nameof(IComponentMiddlewareService));
service.Add<TMiddleware, TComponent>();
}
}

6
Routes.cs Normal file
View File

@ -0,0 +1,6 @@
namespace Connected;
public static class UIRoutes
{
public const string Management = "management";
}

View File

@ -11,16 +11,8 @@ namespace Connected
public BoolConverter()
{
}
protected override bool? ConvertValue(T? value)
{
return OnSet(value);
}
protected override T? ConvertValueBack(bool? value)
{
return OnGet(value);
SetFunc = OnSet;
GetFunc = OnGet;
}
private T OnGet(bool? value)
@ -39,12 +31,12 @@ namespace Connected
return (T)(object)(value == true ? 1 : (value == false ? (int?)0 : null));
else
{
TriggerError($"Conversion to type {typeof(T)} not implemented");
UpdateGetError($"Conversion to type {typeof(T)} not implemented");
}
}
catch (Exception e)
{
TriggerError("Conversion error: " + e.Message);
UpdateGetError("Conversion error: " + e.Message);
return default(T);
}
return default(T);
@ -77,13 +69,13 @@ namespace Connected
}
else
{
TriggerError("Unable to convert to bool? from type " + typeof(T).Name);
UpdateSetError("Unable to convert to bool? from type " + typeof(T).Name);
return null;
}
}
catch (FormatException e)
{
TriggerError("Conversion error: " + e.Message);
UpdateSetError("Conversion error: " + e.Message);
return null;
}
}

View File

@ -1,111 +1,88 @@
using System.Globalization;
using System;
using System.Globalization;
namespace Connected;
public class Converter<TSourceType, TDestinationType>
namespace Connected
{
public event EventHandler<string?>? ErrorOccured;
public class Converter<T, U>
{
public Func<T, U> SetFunc { get; set; }
public Func<U, T> GetFunc { get; set; }
/// <summary>
/// The culture info being used for decimal points, date and time format, etc.
/// </summary>
public CultureInfo Culture { get; set; } = Converters.DefaultCulture;
public TDestinationType? Convert(TSourceType value)
public Action<string> OnError { get; set; }
public bool SetError { get; set; }
public bool GetError { get; set; }
public string SetErrorMessage { get; set; }
public string GetErrorMessage { get; set; }
public U Set(T value)
{
SetError = false;
SetErrorMessage = null;
if (SetFunc == null)
return default(U);
try
{
return ConvertValue(value);
return SetFunc(value);
}
catch (Exception e)
{
TriggerError($"Conversion from {typeof(TSourceType).Name} to {typeof(TDestinationType).Name} failed: {e.Message}", e);
SetError = true;
SetErrorMessage = $"Conversion from {typeof(T).Name} to {typeof(U).Name} failed: {e.Message}";
}
return default(U);
}
return default;
}
protected virtual TDestinationType? ConvertValue(TSourceType? value)
{
return default;
}
public TSourceType? ConvertBack(TDestinationType value)
public T Get(U value)
{
GetError = false;
GetErrorMessage = null;
if (GetFunc == null)
return default(T);
try
{
return ConvertValueBack(value);
return GetFunc(value);
}
catch (Exception e)
{
TriggerError($"Conversion from {typeof(TDestinationType).Name} to {typeof(TSourceType).Name} failed: {e.Message}", e);
GetError = true;
GetErrorMessage = $"Conversion from {typeof(U).Name} to {typeof(T).Name} failed: {e.Message}";
}
return default(T);
}
return default;
}
protected virtual TSourceType? ConvertValueBack(TDestinationType? value)
protected void UpdateSetError(string msg)
{
return default;
SetError = true;
SetErrorMessage = msg;
OnError?.Invoke(msg);
}
protected void TriggerError(string? msg, Exception? e = null)
protected void UpdateGetError(string msg)
{
ErrorOccured?.Invoke(this, msg);
OnErrorOccured(msg, e);
GetError = true;
GetErrorMessage = msg;
OnError?.Invoke(msg);
}
}
protected virtual void OnErrorOccured(string? msg, Exception? e)
/// <summary>
/// Converter from T to string
///
/// Set converts to string
/// Get converts from string
/// </summary>
public class Converter<T> : Converter<T, string>
{
}
}
/// <summary>
/// Converts values using the supplied lambda functions.
/// </summary>
/// <typeparam name="TSourceType">The source type</typeparam>
/// <typeparam name="TDestinationType">The destination type</typeparam>
public class LambdaConverter<TSourceType, TDestinationType> : Converter<TSourceType, TDestinationType>
{
private readonly Func<TSourceType, TDestinationType?>? _convertFunction;
private readonly Func<TDestinationType, TSourceType?>? _convertBackFunction;
public LambdaConverter(Func<TSourceType, TDestinationType?>? convertFunction = null, Func<TDestinationType, TSourceType?>? convertBackFunction = null)
{
_convertFunction = convertFunction;
_convertBackFunction = convertBackFunction;
}
protected override TDestinationType? ConvertValue(TSourceType? value)
{
if (_convertFunction is null)
return base.ConvertValue(value);
return _convertFunction.Invoke(value);
}
protected override TSourceType? ConvertValueBack(TDestinationType? value)
{
if (_convertFunction is null)
return base.ConvertValueBack(value);
return _convertBackFunction.Invoke(value);
}
}
/// <summary>
/// Converter from T to string
///
/// Set converts to string
/// Get converts from string
/// </summary>
public class ToStringConverter<T> : Converter<T, string>
{
/// <summary>
/// Custom Format to be applied on bidirectional way.
/// </summary>
public string? Format { get; set; } = null;
public string Format { get; set; } = null;
}
}

View File

@ -1,8 +1,55 @@
using System.Globalization;
namespace Connected;
public static class Converters
namespace Connected
{
public static CultureInfo DefaultCulture { get; set; } = CultureInfo.CurrentUICulture;
public static class Converters
{
public static CultureInfo DefaultCulture = CultureInfo.CurrentUICulture;
#region --> Date converters
public static Converter<DateTime> IsoDate
=> new()
{ SetFunc = SetIsoDate, GetFunc = GetIsoDate };
private static DateTime GetIsoDate(string value)
{
if (DateTime.TryParse(value, out var dateTime))
return dateTime;
return DateTime.MinValue;
}
private static string SetIsoDate(DateTime value)
{
return value.ToIsoDateString();
}
public static Converter<DateTime?> NullableIsoDate
=> new()
{ SetFunc = SetNullableIsoDate, GetFunc = GetNullableIsoDate };
private static DateTime? GetNullableIsoDate(string value)
{
if (DateTime.TryParse(value, out var dateTime))
return dateTime;
return null;
}
private static string SetNullableIsoDate(DateTime? value)
{
return value.ToIsoDateString();
}
public static DateConverter DateFormat(string format)
{
format ??= "yyyy-MM-dd";
return new DateConverter(format);
}
public static DateConverter DateFormat(string format, CultureInfo culture)
{
return new DateConverter(format) { Culture = culture };
}
#endregion
}
}

View File

@ -1,29 +1,90 @@
namespace Connected;
using System;
/// <summary>
/// A ready made DateTime? to string binding converter with configurable date format and culture
/// </summary>
public class NullableDateConverter : ToStringConverter<DateTime?>
namespace Connected
{
/// <summary>
/// A ready made DateTime? to string binding converter with configurable date format and culture
/// </summary>
public class NullableDateConverter : Converter<DateTime?>
{
public string DateFormat { get; set; }
public NullableDateConverter(string format = "yyyy-MM-dd") => DateFormat = format;
public NullableDateConverter(string format = "yyyy-MM-dd")
{
DateFormat = format;
SetFunc = OnSet;
GetFunc = OnGet;
}
protected override string? ConvertValue(DateTime? value) => value?.ToString(DateFormat, Culture);
private DateTime? OnGet(string arg)
{
try
{
return DateTime.ParseExact(arg, DateFormat, Culture);
}
catch (FormatException e)
{
UpdateGetError(e.Message);
return null;
}
}
private string OnSet(DateTime? arg)
{
if (arg == null)
return null;
try
{
return arg.Value.ToString(DateFormat, Culture);
}
catch (FormatException e)
{
UpdateSetError(e.Message);
return null;
}
}
}
/// <summary>
/// A ready made DateTime to string binding converter with configurable date format and culture
/// </summary>
public class DateConverter : Converter<DateTime>
{
public string DateFormat { get; set; } = "yyyy-MM-dd";
public DateConverter(string format)
{
DateFormat = format;
SetFunc = OnSet;
GetFunc = OnGet;
}
private DateTime OnGet(string arg)
{
try
{
return DateTime.ParseExact(arg, DateFormat, Culture);
}
catch (FormatException e)
{
UpdateGetError(e.Message);
return default;
}
}
private string OnSet(DateTime arg)
{
try
{
return arg.ToString(DateFormat, Culture);
}
catch (FormatException e)
{
UpdateSetError(e.Message);
return null;
}
}
}
protected override DateTime? ConvertValueBack(string? value) => DateTime.ParseExact(value, DateFormat, Culture);
}
/// <summary>
/// A ready made DateTime to string binding converter with configurable date format and culture
/// </summary>
public class DateConverter : ToStringConverter<DateTime>
{
public string DateFormat { get; set; }
public DateConverter(string format = "yyyy-MM-dd") => DateFormat = format;
protected override string? ConvertValue(DateTime value) => value.ToString(DateFormat, Culture);
protected override DateTime ConvertValueBack(string? value) => DateTime.ParseExact(value, DateFormat, Culture);
}

View File

@ -1,200 +1,148 @@
using System.Globalization;
using System;
using System.Globalization;
namespace Connected;
/// <summary>
/// A universal T to string binding converter
/// </summary>
public class DefaultConverter<T> : ToStringConverter<T>
namespace Connected
{
protected override string? ConvertValue(T? value) => ConvertToString(value);
protected override T? ConvertValueBack(string? value) => ConvertFromString(value);
/// <summary>
/// A universal T to string binding converter
/// </summary>
public class DefaultConverter<T> : Converter<T>
{
public DefaultConverter()
{
SetFunc = ConvertToString;
GetFunc = ConvertFromString;
}
public string DefaultTimeSpanFormat { get; set; } = "c";
protected virtual T? ConvertFromString(string? value)
protected virtual T ConvertFromString(string value)
{
try
{
/*
* This is important, or otherwise all the TryParse down there might fail.
*/
if (string.IsNullOrEmpty(value))
{
return default;
}
/*
* String
*/
else if (typeof(T) == typeof(string))
{
// string
if (typeof(T) == typeof(string))
return (T)(object)value;
}
/*
* Char
*/
// this is important, or otherwise all the TryParse down there might fail.
if (string.IsNullOrEmpty(value))
return default(T);
// char
else if (typeof(T) == typeof(char) || typeof(T) == typeof(char?))
{
return (T)(object)value[0];
}
/*
* Bool
*/
// bool
else if (typeof(T) == typeof(bool) || typeof(T) == typeof(bool?))
{
var lowerValue = value.ToLowerInvariant();
if (lowerValue is "true" or "on")
return (T)(object)true;
if (lowerValue is "false" or "off")
return (T)(object)false;
TriggerError("Not a valid boolean");
UpdateGetError("Not a valid boolean");
}
/*
* Sbyte
*/
// sbyte
else if (typeof(T) == typeof(sbyte) || typeof(T) == typeof(sbyte?))
{
if (sbyte.TryParse(value, NumberStyles.Integer | NumberStyles.AllowThousands, Culture, out var parsedValue))
return (T)(object)parsedValue;
TriggerError("Not a valid number");
UpdateGetError("Not a valid number");
}
/*
* Byte
*/
// byte
else if (typeof(T) == typeof(byte) || typeof(T) == typeof(byte?))
{
if (byte.TryParse(value, NumberStyles.Integer | NumberStyles.AllowThousands, Culture, out var parsedValue))
return (T)(object)parsedValue;
TriggerError("Not a valid number");
UpdateGetError("Not a valid number");
}
/*
* Short
*/
// short
else if (typeof(T) == typeof(short) || typeof(T) == typeof(short?))
{
if (short.TryParse(value, NumberStyles.Integer | NumberStyles.AllowThousands, Culture, out var parsedValue))
return (T)(object)parsedValue;
TriggerError("Not a valid number");
UpdateGetError("Not a valid number");
}
/*
* Ushort
*/
// ushort
else if (typeof(T) == typeof(ushort) || typeof(T) == typeof(ushort?))
{
if (ushort.TryParse(value, NumberStyles.Integer | NumberStyles.AllowThousands, Culture, out var parsedValue))
return (T)(object)parsedValue;
TriggerError("Not a valid number");
UpdateGetError("Not a valid number");
}
/*
* Int
*/
// int
else if (typeof(T) == typeof(int) || typeof(T) == typeof(int?))
{
if (int.TryParse(value, NumberStyles.Integer | NumberStyles.AllowThousands, Culture, out var parsedValue))
return (T)(object)parsedValue;
TriggerError("Not a valid number");
UpdateGetError("Not a valid number");
}
/*
* Uint
*/
// uint
else if (typeof(T) == typeof(uint) || typeof(T) == typeof(uint?))
{
if (uint.TryParse(value, NumberStyles.Integer | NumberStyles.AllowThousands, Culture, out var parsedValue))
return (T)(object)parsedValue;
TriggerError("Not a valid number");
UpdateGetError("Not a valid number");
}
/*
* Long
*/
// long
else if (typeof(T) == typeof(long) || typeof(T) == typeof(long?))
{
if (long.TryParse(value, NumberStyles.Integer | NumberStyles.AllowThousands, Culture, out var parsedValue))
return (T)(object)parsedValue;
TriggerError("Not a valid number");
UpdateGetError("Not a valid number");
}
/*
* Ulong
*/
// ulong
else if (typeof(T) == typeof(ulong) || typeof(T) == typeof(ulong?))
{
if (ulong.TryParse(value, NumberStyles.Integer | NumberStyles.AllowThousands, Culture, out var parsedValue))
return (T)(object)parsedValue;
TriggerError("Not a valid number");
UpdateGetError("Not a valid number");
}
/*
* Float
*/
// float
else if (typeof(T) == typeof(float) || typeof(T) == typeof(float?))
{
if (float.TryParse(value, NumberStyles.Any, Culture, out var parsedValue))
return (T)(object)parsedValue;
TriggerError("Not a valid number");
UpdateGetError("Not a valid number");
}
/*
* Double
*/
// double
else if (typeof(T) == typeof(double) || typeof(T) == typeof(double?))
{
if (double.TryParse(value, NumberStyles.Any, Culture, out var parsedValue))
return (T)(object)parsedValue;
TriggerError("Not a valid number");
UpdateGetError("Not a valid number");
}
/*
* Decimal
*/
// decimal
else if (typeof(T) == typeof(decimal) || typeof(T) == typeof(decimal?))
{
if (decimal.TryParse(value, NumberStyles.Any, Culture, out var parsedValue))
return (T)(object)parsedValue;
TriggerError("Not a valid number");
UpdateGetError("Not a valid number");
}
/*
* Guid
*/
// guid
else if (typeof(T) == typeof(Guid) || typeof(T) == typeof(Guid?))
{
if (Guid.TryParse(value, out var parsedValue))
return (T)(object)parsedValue;
TriggerError("Not a valid GUID");
UpdateGetError("Not a valid GUID");
}
/*
* Enum
*/
// enum
else if (IsNullableEnum(typeof(T)))
{
var enum_type = Nullable.GetUnderlyingType(typeof(T));
if (Enum.TryParse(enum_type, value, out var parsedValue))
return (T)parsedValue;
TriggerError("Not a value of " + enum_type.Name);
UpdateGetError("Not a value of " + enum_type.Name);
}
else if (typeof(T).IsEnum)
{
if (Enum.TryParse(typeof(T), value, out var parsedValue))
return (T)parsedValue;
TriggerError("Not a value of " + typeof(T).Name);
UpdateGetError("Not a value of " + typeof(T).Name);
}
/*
* DateTime
*/
// datetime
else if (typeof(T) == typeof(DateTime) || typeof(T) == typeof(DateTime?))
{
try
@ -203,12 +151,10 @@ public class DefaultConverter<T> : ToStringConverter<T>
}
catch (FormatException)
{
TriggerError("Not a valid date time");
UpdateGetError("Not a valid date time");
}
}
/*
* Timespan
*/
// timespan
else if (typeof(T) == typeof(TimeSpan) || typeof(T) == typeof(TimeSpan?))
{
try
@ -217,185 +163,97 @@ public class DefaultConverter<T> : ToStringConverter<T>
}
catch (FormatException)
{
TriggerError("Not a valid time span");
UpdateGetError("Not a valid time span");
}
}
else
{
TriggerError($"Conversion to type {typeof(T)} not implemented");
UpdateGetError($"Conversion to type {typeof(T)} not implemented");
}
}
catch (Exception e)
{
TriggerError("Conversion error: " + e.Message);
UpdateGetError("Conversion error: " + e.Message);
}
return default;
return default(T);
}
protected virtual string? ConvertToString(T? arg)
protected virtual string ConvertToString(T arg)
{
/*
* This catches all null values. No additional null checks necessary.
*/
if (arg is null)
return null;
if (arg == null)
return null; // <-- this catches all nullable values which are null. no nullchecks necessary below!
try
{
/*
* String
*/
// string
if (typeof(T) == typeof(string))
{
return (string)(object)arg;
}
/*
* Char
*/
else if (typeof(T) == typeof(char))
{
// char
if (typeof(T) == typeof(char))
return ((char)(object)arg).ToString(Culture);
}
else if (typeof(T) == typeof(char?))
{
if (typeof(T) == typeof(char?))
return ((char?)(object)arg).Value.ToString(Culture);
}
/*
* Bool
*/
else if (typeof(T) == typeof(bool))
{
// bool
if (typeof(T) == typeof(bool))
return ((bool)(object)arg).ToString(CultureInfo.InvariantCulture);
}
else if (typeof(T) == typeof(bool?))
{
if (typeof(T) == typeof(bool?))
return ((bool?)(object)arg).Value.ToString(CultureInfo.InvariantCulture);
}
/*
* Sbyte
*/
else if (typeof(T) == typeof(sbyte))
{
// sbyte
if (typeof(T) == typeof(sbyte))
return ((sbyte)(object)arg).ToString(Format, Culture);
}
else if (typeof(T) == typeof(sbyte?))
{
if (typeof(T) == typeof(sbyte?))
return ((sbyte?)(object)arg).Value.ToString(Format, Culture);
}
/*
* Byte
*/
else if (typeof(T) == typeof(byte))
{
// byte
if (typeof(T) == typeof(byte))
return ((byte)(object)arg).ToString(Format, Culture);
}
else if (typeof(T) == typeof(byte?))
{
if (typeof(T) == typeof(byte?))
return ((byte?)(object)arg).Value.ToString(Format, Culture);
}
/*
* Short
*/
else if (typeof(T) == typeof(short))
{
// short
if (typeof(T) == typeof(short))
return ((short)(object)arg).ToString(Format, Culture);
}
else if (typeof(T) == typeof(short?))
{
if (typeof(T) == typeof(short?))
return ((short?)(object)arg).Value.ToString(Format, Culture);
}
/*
* Ushort
*/
else if (typeof(T) == typeof(ushort))
{
// ushort
if (typeof(T) == typeof(ushort))
return ((ushort)(object)arg).ToString(Format, Culture);
}
else if (typeof(T) == typeof(ushort?))
{
if (typeof(T) == typeof(ushort?))
return ((ushort?)(object)arg).Value.ToString(Format, Culture);
}
/*
* Int
*/
// int
else if (typeof(T) == typeof(int))
{
return ((int)(object)arg).ToString(Format, Culture);
}
else if (typeof(T) == typeof(int?))
{
return ((int?)(object)arg).Value.ToString(Format, Culture);
}
/*
* Uint
*/
// uint
else if (typeof(T) == typeof(uint))
{
return ((uint)(object)arg).ToString(Format, Culture);
}
else if (typeof(T) == typeof(uint?))
{
return ((uint?)(object)arg).Value.ToString(Format, Culture);
}
/*
* Long
*/
// long
else if (typeof(T) == typeof(long))
{
return ((long)(object)arg).ToString(Format, Culture);
}
else if (typeof(T) == typeof(long?))
{
return ((long?)(object)arg).Value.ToString(Format, Culture);
}
/*
* Ulong
*/
// ulong
else if (typeof(T) == typeof(ulong))
{
return ((ulong)(object)arg).ToString(Format, Culture);
}
else if (typeof(T) == typeof(ulong?))
{
return ((ulong?)(object)arg).Value.ToString(Format, Culture);
}
/*
* Float
*/
// float
else if (typeof(T) == typeof(float))
{
return ((float)(object)arg).ToString(Format, Culture);
}
else if (typeof(T) == typeof(float?))
{
return ((float?)(object)arg).Value.ToString(Format, Culture);
}
/*
* Double
*/
// double
else if (typeof(T) == typeof(double))
{
return ((double)(object)arg).ToString(Format, Culture);
}
else if (typeof(T) == typeof(double?))
{
return ((double?)(object)arg).Value.ToString(Format, Culture);
}
/*
* Decimal
*/
// decimal
else if (typeof(T) == typeof(decimal))
{
return ((decimal)(object)arg).ToString(Format, Culture);
}
else if (typeof(T) == typeof(decimal?))
{
return ((decimal?)(object)arg).Value.ToString(Format, Culture);
}
/*
* Guid
*/
// guid
else if (typeof(T) == typeof(Guid))
{
var value = (Guid)(object)arg;
@ -406,9 +264,7 @@ public class DefaultConverter<T> : ToStringConverter<T>
var value = (Guid?)(object)arg;
return value.Value.ToString();
}
/*
* Enum
*/
// enum
else if (IsNullableEnum(typeof(T)))
{
var value = (Enum)(object)arg;
@ -419,9 +275,7 @@ public class DefaultConverter<T> : ToStringConverter<T>
var value = (Enum)(object)arg;
return value.ToString();
}
/*
* DateTime
*/
// datetime
else if (typeof(T) == typeof(DateTime))
{
var value = (DateTime)(object)arg;
@ -432,9 +286,7 @@ public class DefaultConverter<T> : ToStringConverter<T>
var value = (DateTime?)(object)arg;
return value.Value.ToString(Format ?? Culture.DateTimeFormat.ShortDatePattern, Culture);
}
/*
* Timespan
*/
// timespan
else if (typeof(T) == typeof(TimeSpan))
{
var value = (TimeSpan)(object)arg;
@ -445,12 +297,11 @@ public class DefaultConverter<T> : ToStringConverter<T>
var value = (TimeSpan?)(object)arg;
return value.Value.ToString(Format ?? DefaultTimeSpanFormat, Culture);
}
return arg.ToString();
}
catch (FormatException e)
{
TriggerError("Conversion error: " + e.Message);
UpdateSetError("Conversion error: " + e.Message);
return null;
}
}
@ -458,7 +309,7 @@ public class DefaultConverter<T> : ToStringConverter<T>
public static bool IsNullableEnum(Type t)
{
var u = Nullable.GetUnderlyingType(t);
return (u is not null) && u.IsEnum;
return (u != null) && u.IsEnum;
}
}
}

View File

@ -1,17 +0,0 @@
using Connected.Extensions;
namespace Connected;
public class IsoDateConverter : Converter<DateTime, string>
{
protected override string ConvertValue(DateTime value) => value.ToIsoDateString();
protected override DateTime ConvertValueBack(string? value) => DateTime.TryParse(value, out var dateTime) ? dateTime : DateTime.MinValue;
}
public class NullableIsoDateConverter : Converter<DateTime?, string?>
{
protected override string? ConvertValue(DateTime? value) => value?.ToIsoDateString();
protected override DateTime? ConvertValueBack(string? value) => DateTime.TryParse(value, out var dateTime) ? dateTime : null;
}

View File

@ -1,295 +1,191 @@
using System.Diagnostics.CodeAnalysis;
using System;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
namespace Connected;
/// <summary>
/// A universal T to double binding converter
///
/// Note: currently not in use. Should we ever use it, remove
/// the [ExcludeFromCodeCoverage] attribute
/// </summary>
[ExcludeFromCodeCoverage]
public class NumericConverter<T> : Converter<T, double>
namespace Connected
{
protected override double ConvertValue(T? value)
/// <summary>
/// A universal T to double binding converter
///
/// Note: currently not in use. Should we ever use it, remove
/// the [ExcludeFromCodeCoverage] attribute
/// </summary>
[ExcludeFromCodeCoverage]
public class NumericConverter<T> : Converter<T, double>
{
if (value is null)
public NumericConverter()
{
return double.NaN;
}
/*
* Double
*/
else if (typeof(T) == typeof(double))
{
return (double)(object)value;
}
else if (typeof(T) == typeof(double?))
{
return ((double?)(object)value).Value;
}
/*
* String
*/
else if (typeof(T) == typeof(string))
{
return double.Parse((string)(object)value, NumberStyles.Any, Culture);
}
/*
* SByte
*/
else if (typeof(T) == typeof(sbyte))
{
return System.Convert.ToDouble((sbyte)(object)value);
}
else if (typeof(T) == typeof(sbyte?))
{
return System.Convert.ToDouble(((sbyte?)(object)value).Value);
}
/*
* Byte
*/
else if (typeof(T) == typeof(byte))
{
return System.Convert.ToDouble((byte)(object)value);
}
else if (typeof(T) == typeof(byte?))
{
return System.Convert.ToDouble(((byte?)(object)value).Value);
}
/*
* Short
*/
else if (typeof(T) == typeof(short))
{
return System.Convert.ToDouble((short)(object)value);
}
else if (typeof(T) == typeof(short?))
{
return System.Convert.ToDouble(((short?)(object)value).Value);
}
/*
* Ushort
*/
else if (typeof(T) == typeof(ushort))
{
return System.Convert.ToDouble((ushort)(object)value);
}
else if (typeof(T) == typeof(ushort?))
{
return System.Convert.ToDouble(((ushort?)(object)value).Value);
}
/*
* Int
*/
else if (typeof(T) == typeof(int))
{
return System.Convert.ToDouble((int)(object)value);
}
else if (typeof(T) == typeof(int?))
{
return System.Convert.ToDouble(((int?)(object)value).Value);
}
/*
* Uint
*/
else if (typeof(T) == typeof(uint))
{
return System.Convert.ToDouble((uint)(object)value);
}
else if (typeof(T) == typeof(uint?))
{
return System.Convert.ToDouble(((uint?)(object)value).Value);
}
/*
* Long
*/
else if (typeof(T) == typeof(long))
{
return System.Convert.ToDouble((long)(object)value);
}
else if (typeof(T) == typeof(long?))
{
return System.Convert.ToDouble(((long?)(object)value).Value);
}
/*
* Ulong
*/
else if (typeof(T) == typeof(ulong))
{
return System.Convert.ToDouble((ulong)(object)value);
}
else if (typeof(T) == typeof(ulong?))
{
return System.Convert.ToDouble(((ulong?)(object)value).Value);
}
/*
* Float
*/
else if (typeof(T) == typeof(float))
{
return System.Convert.ToDouble((float)(object)value);
}
else if (typeof(T) == typeof(float?))
{
return System.Convert.ToDouble(((float?)(object)value).Value);
}
/*
* Deimal
*/
else if (typeof(T) == typeof(decimal))
{
return System.Convert.ToDouble((decimal)(object)value);
}
else if (typeof(T) == typeof(decimal?))
{
return System.Convert.ToDouble(((decimal?)(object)value).Value);
}
else
{
TriggerError("Unable to convert to double from type " + typeof(T).Name);
return double.NaN;
}
SetFunc = OnSet;
GetFunc = OnGet;
}
protected override T? ConvertValueBack(double value)
private T OnGet(double value)
{
/*
* Double
*/
try
{
// double
if (typeof(T) == typeof(double) || typeof(T) == typeof(double?))
{
return (T)(object)value;
}
/*
* String
*/
// string
else if (typeof(T) == typeof(string))
{
return (T)(object)value.ToString(Culture);
}
/*
* Sbyte
*/
// sbyte
else if (typeof(T) == typeof(sbyte) || typeof(T) == typeof(sbyte?))
{
return (T)(object)System.Convert.ToSByte(value);
}
/*
* Byte
*/
return (T)(object)Convert.ToSByte(value);
// byte
else if (typeof(T) == typeof(byte) || typeof(T) == typeof(byte?))
{
return (T)(object)System.Convert.ToByte(value);
}
/*
* Short
*/
return (T)(object)Convert.ToByte(value);
// short
else if (typeof(T) == typeof(short) || typeof(T) == typeof(short?))
{
return (T)(object)System.Convert.ToInt16(value);
}
/*
* Ushort
*/
return (T)(object)Convert.ToInt16(value);
// ushort
else if (typeof(T) == typeof(ushort) || typeof(T) == typeof(ushort?))
{
return (T)(object)System.Convert.ToUInt16(value);
}
/*
* Int
*/
return (T)(object)Convert.ToUInt16(value);
// int
else if (typeof(T) == typeof(int) || typeof(T) == typeof(int?))
{
return (T)(object)System.Convert.ToInt32(value);
}
/*
* Uint
*/
return (T)(object)Convert.ToInt32(value);
// uint
else if (typeof(T) == typeof(uint) || typeof(T) == typeof(uint?))
{
return (T)(object)System.Convert.ToUInt32(value);
}
/*
* Long
*/
return (T)(object)Convert.ToUInt32(value);
// long
else if (typeof(T) == typeof(long) || typeof(T) == typeof(long?))
{
return (T)(object)System.Convert.ToInt64(value);
}
/*
* Ulong
*/
return (T)(object)Convert.ToInt64(value);
// ulong
else if (typeof(T) == typeof(ulong) || typeof(T) == typeof(ulong?))
{
return (T)(object)System.Convert.ToUInt64(value);
}
/*
* Float
*/
return (T)(object)Convert.ToUInt64(value);
// float
else if (typeof(T) == typeof(float) || typeof(T) == typeof(float?))
{
return (T)(object)System.Convert.ToSingle(value);
}
/*
* Decimal
*/
return (T)(object)Convert.ToSingle(value);
// decimal
else if (typeof(T) == typeof(decimal) || typeof(T) == typeof(decimal?))
{
return (T)(object)System.Convert.ToDecimal(value);
}
return (T)(object)Convert.ToDecimal(value);
else
{
TriggerError($"Conversion to type {typeof(T)} not implemented");
return default;
UpdateGetError($"Conversion to type {typeof(T)} not implemented");
}
}
catch (Exception e)
{
UpdateGetError("Conversion error: " + e.Message);
return default(T);
}
return default(T);
}
private double OnSet(T arg)
{
if (arg == null)
return double.NaN; // <-- this catches all nullable values which are null. no nullchecks necessary below!
try
{
// double
if (typeof(T) == typeof(double))
return (double)(object)arg;
else if (typeof(T) == typeof(double?))
return ((double?)(object)arg).Value;
// string
if (typeof(T) == typeof(string))
return double.Parse((string)(object)arg, NumberStyles.Any, Culture);
// sbyte
if (typeof(T) == typeof(sbyte))
return Convert.ToDouble((sbyte)(object)arg);
if (typeof(T) == typeof(sbyte?))
return Convert.ToDouble(((sbyte?)(object)arg).Value);
// byte
if (typeof(T) == typeof(byte))
return Convert.ToDouble((byte)(object)arg);
if (typeof(T) == typeof(byte?))
return Convert.ToDouble(((byte?)(object)arg).Value);
// short
if (typeof(T) == typeof(short))
return Convert.ToDouble((short)(object)arg);
if (typeof(T) == typeof(short?))
return Convert.ToDouble(((short?)(object)arg).Value);
// ushort
if (typeof(T) == typeof(ushort))
return Convert.ToDouble((ushort)(object)arg);
if (typeof(T) == typeof(ushort?))
return Convert.ToDouble(((ushort?)(object)arg).Value);
// int
else if (typeof(T) == typeof(int))
return Convert.ToDouble((int)(object)arg);
else if (typeof(T) == typeof(int?))
return Convert.ToDouble(((int?)(object)arg).Value);
// uint
else if (typeof(T) == typeof(uint))
return Convert.ToDouble((uint)(object)arg);
else if (typeof(T) == typeof(uint?))
return Convert.ToDouble(((uint?)(object)arg).Value);
// long
else if (typeof(T) == typeof(long))
return Convert.ToDouble((long)(object)arg);
else if (typeof(T) == typeof(long?))
return Convert.ToDouble(((long?)(object)arg).Value);
// ulong
else if (typeof(T) == typeof(ulong))
return Convert.ToDouble((ulong)(object)arg);
else if (typeof(T) == typeof(ulong?))
return Convert.ToDouble(((ulong?)(object)arg).Value);
// float
else if (typeof(T) == typeof(float))
return Convert.ToDouble((float)(object)arg);
else if (typeof(T) == typeof(float?))
return Convert.ToDouble(((float?)(object)arg).Value);
// decimal
else if (typeof(T) == typeof(decimal))
return Convert.ToDouble((decimal)(object)arg);
else if (typeof(T) == typeof(decimal?))
return Convert.ToDouble(((decimal?)(object)arg).Value);
else
{
UpdateSetError("Unable to convert to double from type " + typeof(T).Name);
return double.NaN;
}
}
catch (FormatException e)
{
UpdateSetError("Conversion error: " + e.Message);
return double.NaN;
}
}
#region Floating Point comparison
private const double MinNormal = 2.2250738585072014E-308d;
#region --> Floating Point comparison
const double MinNormal = 2.2250738585072014E-308d;
public static bool AreEqual(double a, double b, double epsilon = MinNormal)
{
/*
* Copyright (c) Michael Borgwardt
*/
// Copyright (c) Michael Borgwardt
var absA = Math.Abs(a);
var absB = Math.Abs(b);
var diff = Math.Abs(a - b);
/*
* Shortcut, handles infinities
*/
if (a.Equals(b))
{
{ // shortcut, handles infinities
return true;
}
else if (a == 0 || b == 0 || absA + absB < MinNormal)
{
/*
* a or b is zero or both are extremely close to it
* relative error is less meaningful here
*/
return diff < epsilon * MinNormal;
// a or b is zero or both are extremely close to it
// relative error is less meaningful here
return diff < (epsilon * MinNormal);
}
else
{
/*
* Use relative error
*/
{ // use relative error
return diff / (absA + absB) < epsilon;
}
}
#endregion
}
}
[ExcludeFromCodeCoverage]
internal static class Number
{
public static T? To<T>(double d)
[ExcludeFromCodeCoverage]
internal static class Num
{
public static T To<T>(double d)
{
if (typeof(T) == typeof(sbyte) && d >= sbyte.MinValue && sbyte.MaxValue >= d)
return (T)(object)Convert.ToSByte(d);
@ -335,7 +231,6 @@ internal static class Number
return (T)(object)Convert.ToDouble(d);
if (typeof(T) == typeof(decimal?) && (decimal)d >= decimal.MinValue && decimal.MaxValue >= (decimal)d)
return (T)(object)Convert.ToDecimal(d);
return default;
}
public static double From<T>(T v)
@ -384,7 +279,7 @@ internal static class Number
return Convert.ToDouble((double?)(object)v);
if (typeof(T) == typeof(decimal?))
return Convert.ToDouble((decimal?)(object)v);
return default;
}
}
}

View File

@ -1,41 +1,61 @@
using Connected.Components;
namespace Connected.Utilities.BindingConverters;
public class RangeConverter<T> : ToStringConverter<Range<T>>
namespace Connected
{
private readonly DefaultConverter<T> _converter;
public RangeConverter() => _converter = new DefaultConverter<T>();
protected override string? ConvertValue(Range<T>? value) => value is not null
? Join(_converter.Convert(value.Start), _converter.Convert(value.End))
: string.Empty;
protected override Range<T>? ConvertValueBack(string? value)
public class RangeConverter<T> : Converter<Components.Range<T>>
{
return !Split(value, out var valueStart, out var valueEnd)
? null
: new Range<T>(_converter.ConvertBack(valueStart), _converter.ConvertBack(valueEnd));
readonly DefaultConverter<T> _converter;
public RangeConverter()
{
_converter = new DefaultConverter<T>();
SetFunc = OnSet;
GetFunc = OnGet;
}
public static string Join(string? valueStart, string? valueEnd) => string.IsNullOrEmpty(valueStart) && string.IsNullOrEmpty(valueEnd) ? string.Empty : $"[{valueStart};{valueEnd}]";
private Components.Range<T> OnGet(string value)
{
if (!Split(value, out var valueStart, out var valueEnd))
return null;
public static bool Split(string? value, out string? valueStart, out string? valueEnd)
return new Components.Range<T>(_converter.Get(valueStart), _converter.Get(valueEnd));
}
private string OnSet(Components.Range<T> arg)
{
if (arg == null)
return string.Empty;
return Join(_converter.Set(arg.Start), _converter.Set(arg.End));
}
public static string Join(string valueStart, string valueEnd)
{
if (string.IsNullOrEmpty(valueStart) && string.IsNullOrEmpty(valueEnd))
return string.Empty;
return $"[{valueStart};{valueEnd}]";
}
public static bool Split(string value, out string valueStart, out string valueEnd)
{
valueStart = valueEnd = string.Empty;
if (string.IsNullOrEmpty(value) || value[0] != '[' || value[^1] != ']')
{
return false;
}
var idx = value.IndexOf(';');
if (idx < 1)
{
return false;
}
valueStart = value[1..idx];
valueEnd = value[(idx + 1)..^1];
return true;
}
}
}