diff --git a/.gitremotes b/.gitremotes
index 5d34203..bd825d4 100644
--- a/.gitremotes
+++ b/.gitremotes
@@ -1,10 +1,2 @@
-FSI.Lib http://desiaugetc7-088:3000/FSI/FSI.Lib.git (fetch)
-FSI.Lib http://desiaugetc7-088:3000/FSI/FSI.Lib.git (push)
-NHotkey http://desiaugetc7-088:3000/FSI/NHotkey.git (fetch)
-NHotkey http://desiaugetc7-088:3000/FSI/NHotkey.git (push)
-NotifyIconWpf http://desiaugetc7-088:3000/FSI/NotifyIconWpf.git (fetch)
-NotifyIconWpf http://desiaugetc7-088:3000/FSI/NotifyIconWpf.git (push)
-RadialMenu http://desiaugetc7-088:3000/FSI/RadialMenu.git (fetch)
-RadialMenu http://desiaugetc7-088:3000/FSI/RadialMenu.git (push)
-origin http://desiaugetc7-088:3000/FSI/FSI.Tools.git (fetch)
-origin http://desiaugetc7-088:3000/FSI/FSI.Tools.git (push)
+origin https://git.stephanmaier.duckdns.org/FSI.BT.IR/FSI.BT.IR.Tools.git (fetch)
+origin https://git.stephanmaier.duckdns.org/FSI.BT.IR/FSI.BT.IR.Tools.git (push)
diff --git a/AutoCompleteTextBox/AutoCompleteTextBox.csproj b/AutoCompleteTextBox/AutoCompleteTextBox.csproj
new file mode 100644
index 0000000..68125d9
--- /dev/null
+++ b/AutoCompleteTextBox/AutoCompleteTextBox.csproj
@@ -0,0 +1,18 @@
+
+
+
+ false
+ net6.0-windows
+ true
+ 1.6.0.0
+ https://github.com/quicoli/WPF-AutoComplete-TextBox
+
+ https://github.com/quicoli/WPF-AutoComplete-TextBox
+ wpf, autocomplete, usercontrol
+ true
+
+ Better support for keyboard focus
+
+ An auto complete textbox and combo box for WPF
+
+
diff --git a/AutoCompleteTextBox/AutoCompleteTextBox.ico b/AutoCompleteTextBox/AutoCompleteTextBox.ico
new file mode 100644
index 0000000..4cb9958
Binary files /dev/null and b/AutoCompleteTextBox/AutoCompleteTextBox.ico differ
diff --git a/AutoCompleteTextBox/BindingEvaluator.cs b/AutoCompleteTextBox/BindingEvaluator.cs
new file mode 100644
index 0000000..6b06a7c
--- /dev/null
+++ b/AutoCompleteTextBox/BindingEvaluator.cs
@@ -0,0 +1,49 @@
+using System.Windows;
+using System.Windows.Data;
+
+namespace AutoCompleteTextBox
+{
+ public class BindingEvaluator : FrameworkElement
+ {
+
+ #region "Fields"
+
+
+ public static readonly DependencyProperty ValueProperty = DependencyProperty.Register("Value", typeof(string), typeof(BindingEvaluator), new FrameworkPropertyMetadata(string.Empty));
+
+ #endregion
+
+ #region "Constructors"
+
+ public BindingEvaluator(Binding binding)
+ {
+ ValueBinding = binding;
+ }
+
+ #endregion
+
+ #region "Properties"
+
+ public string Value
+ {
+ get => (string)GetValue(ValueProperty);
+
+ set => SetValue(ValueProperty, value);
+ }
+
+ public Binding ValueBinding { get; set; }
+
+ #endregion
+
+ #region "Methods"
+
+ public string Evaluate(object dataItem)
+ {
+ DataContext = dataItem;
+ SetBinding(ValueProperty, ValueBinding);
+ return Value;
+ }
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/AutoCompleteTextBox/Editors/AutoCompleteComboBox.cs b/AutoCompleteTextBox/Editors/AutoCompleteComboBox.cs
new file mode 100644
index 0000000..2b65103
--- /dev/null
+++ b/AutoCompleteTextBox/Editors/AutoCompleteComboBox.cs
@@ -0,0 +1,612 @@
+using System;
+using System.Collections;
+using System.Threading;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Controls.Primitives;
+using System.Windows.Data;
+using System.Windows.Input;
+using System.Windows.Media;
+using System.Windows.Threading;
+
+namespace AutoCompleteTextBox.Editors
+{
+ [TemplatePart(Name = PartEditor, Type = typeof(TextBox))]
+ [TemplatePart(Name = PartPopup, Type = typeof(Popup))]
+ [TemplatePart(Name = PartSelector, Type = typeof(Selector))]
+ [TemplatePart(Name = PartExpander, Type = typeof(Expander))]
+ public class AutoCompleteComboBox : Control
+ {
+
+ #region "Fields"
+
+ public const string PartEditor = "PART_Editor";
+ public const string PartPopup = "PART_Popup";
+
+ public const string PartSelector = "PART_Selector";
+ public const string PartExpander = "PART_Expander";
+ public static readonly DependencyProperty DelayProperty = DependencyProperty.Register("Delay", typeof(int), typeof(AutoCompleteComboBox), new FrameworkPropertyMetadata(200));
+ public static readonly DependencyProperty DisplayMemberProperty = DependencyProperty.Register("DisplayMember", typeof(string), typeof(AutoCompleteComboBox), new FrameworkPropertyMetadata(string.Empty));
+ public static readonly DependencyProperty IconPlacementProperty = DependencyProperty.Register("IconPlacement", typeof(IconPlacement), typeof(AutoCompleteComboBox), new FrameworkPropertyMetadata(IconPlacement.Left));
+ public static readonly DependencyProperty IconProperty = DependencyProperty.Register("Icon", typeof(object), typeof(AutoCompleteComboBox), new FrameworkPropertyMetadata(null));
+ public static readonly DependencyProperty IconVisibilityProperty = DependencyProperty.Register("IconVisibility", typeof(Visibility), typeof(AutoCompleteComboBox), new FrameworkPropertyMetadata(Visibility.Visible));
+ public static readonly DependencyProperty IsDropDownOpenProperty = DependencyProperty.Register("IsDropDownOpen", typeof(bool), typeof(AutoCompleteComboBox), new FrameworkPropertyMetadata(false));
+ public static readonly DependencyProperty IsLoadingProperty = DependencyProperty.Register("IsLoading", typeof(bool), typeof(AutoCompleteComboBox), new FrameworkPropertyMetadata(false));
+ public static readonly DependencyProperty IsReadOnlyProperty = DependencyProperty.Register("IsReadOnly", typeof(bool), typeof(AutoCompleteComboBox), new FrameworkPropertyMetadata(false));
+ public static readonly DependencyProperty ItemTemplateProperty = DependencyProperty.Register("ItemTemplate", typeof(DataTemplate), typeof(AutoCompleteComboBox), new FrameworkPropertyMetadata(null));
+ public static readonly DependencyProperty ItemTemplateSelectorProperty = DependencyProperty.Register("ItemTemplateSelector", typeof(DataTemplateSelector), typeof(AutoCompleteComboBox));
+ public static readonly DependencyProperty LoadingContentProperty = DependencyProperty.Register("LoadingContent", typeof(object), typeof(AutoCompleteComboBox), new FrameworkPropertyMetadata(null));
+ public static readonly DependencyProperty ProviderProperty = DependencyProperty.Register("Provider", typeof(IComboSuggestionProvider), typeof(AutoCompleteComboBox), new FrameworkPropertyMetadata(null));
+ public static readonly DependencyProperty SelectedItemProperty = DependencyProperty.Register("SelectedItem", typeof(object), typeof(AutoCompleteComboBox), new FrameworkPropertyMetadata(null, OnSelectedItemChanged));
+ public static readonly DependencyProperty TextProperty = DependencyProperty.Register("Text", typeof(string), typeof(AutoCompleteComboBox), new FrameworkPropertyMetadata(string.Empty, propertyChangedCallback: null, coerceValueCallback: null, isAnimationProhibited: false, defaultUpdateSourceTrigger: UpdateSourceTrigger.LostFocus, flags: FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
+ public static readonly DependencyProperty FilterProperty = DependencyProperty.Register("Filter", typeof(string), typeof(AutoCompleteComboBox), new FrameworkPropertyMetadata(string.Empty));
+ public static readonly DependencyProperty MaxLengthProperty = DependencyProperty.Register("MaxLength", typeof(int), typeof(AutoCompleteComboBox), new FrameworkPropertyMetadata(0));
+ public static readonly DependencyProperty CharacterCasingProperty = DependencyProperty.Register("CharacterCasing", typeof(CharacterCasing), typeof(AutoCompleteComboBox), new FrameworkPropertyMetadata(CharacterCasing.Normal));
+ public static readonly DependencyProperty MaxPopUpHeightProperty = DependencyProperty.Register("MaxPopUpHeight", typeof(int), typeof(AutoCompleteComboBox), new FrameworkPropertyMetadata(600));
+ public static readonly DependencyProperty MaxPopUpWidthProperty = DependencyProperty.Register("MaxPopUpWidth", typeof(int), typeof(AutoCompleteComboBox), new FrameworkPropertyMetadata(2000));
+
+ public static readonly DependencyProperty WatermarkProperty = DependencyProperty.Register("Watermark", typeof(string), typeof(AutoCompleteComboBox), new FrameworkPropertyMetadata(string.Empty));
+
+ public static readonly DependencyProperty SuggestionBackgroundProperty = DependencyProperty.Register("SuggestionBackground", typeof(Brush), typeof(AutoCompleteComboBox), new FrameworkPropertyMetadata(Brushes.White));
+ private bool _isUpdatingText;
+ private bool _selectionCancelled;
+
+ private SuggestionsAdapter _suggestionsAdapter;
+
+
+ #endregion
+
+ #region "Constructors"
+
+ static AutoCompleteComboBox()
+ {
+ DefaultStyleKeyProperty.OverrideMetadata(typeof(AutoCompleteComboBox), new FrameworkPropertyMetadata(typeof(AutoCompleteComboBox)));
+ }
+
+ #endregion
+
+ #region "Properties"
+
+
+ public int MaxPopupHeight
+ {
+ get => (int)GetValue(MaxPopUpHeightProperty);
+ set => SetValue(MaxPopUpHeightProperty, value);
+ }
+ public int MaxPopupWidth
+ {
+ get => (int)GetValue(MaxPopUpWidthProperty);
+ set => SetValue(MaxPopUpWidthProperty, value);
+ }
+
+
+ public BindingEvaluator BindingEvaluator { get; set; }
+
+ public CharacterCasing CharacterCasing
+ {
+ get => (CharacterCasing)GetValue(CharacterCasingProperty);
+ set => SetValue(CharacterCasingProperty, value);
+ }
+
+ public int MaxLength
+ {
+ get => (int)GetValue(MaxLengthProperty);
+ set => SetValue(MaxLengthProperty, value);
+ }
+
+ public int Delay
+ {
+ get => (int)GetValue(DelayProperty);
+
+ set => SetValue(DelayProperty, value);
+ }
+
+ public string DisplayMember
+ {
+ get => (string)GetValue(DisplayMemberProperty);
+
+ set => SetValue(DisplayMemberProperty, value);
+ }
+
+ public TextBox Editor { get; set; }
+ public Expander Expander { get; set; }
+
+ public DispatcherTimer FetchTimer { get; set; }
+
+ public string Filter
+ {
+ get => (string)GetValue(FilterProperty);
+
+ set => SetValue(FilterProperty, value);
+ }
+
+ public object Icon
+ {
+ get => GetValue(IconProperty);
+
+ set => SetValue(IconProperty, value);
+ }
+
+ public IconPlacement IconPlacement
+ {
+ get => (IconPlacement)GetValue(IconPlacementProperty);
+
+ set => SetValue(IconPlacementProperty, value);
+ }
+
+ public Visibility IconVisibility
+ {
+ get => (Visibility)GetValue(IconVisibilityProperty);
+
+ set => SetValue(IconVisibilityProperty, value);
+ }
+
+ public bool IsDropDownOpen
+ {
+ get => (bool)GetValue(IsDropDownOpenProperty);
+
+ set
+ {
+ this.Expander.IsExpanded = value;
+ SetValue(IsDropDownOpenProperty, value);
+ }
+ }
+
+ public bool IsLoading
+ {
+ get => (bool)GetValue(IsLoadingProperty);
+
+ set => SetValue(IsLoadingProperty, value);
+ }
+
+ public bool IsReadOnly
+ {
+ get => (bool)GetValue(IsReadOnlyProperty);
+
+ set => SetValue(IsReadOnlyProperty, value);
+ }
+
+ public Selector ItemsSelector { get; set; }
+
+ public DataTemplate ItemTemplate
+ {
+ get => (DataTemplate)GetValue(ItemTemplateProperty);
+
+ set => SetValue(ItemTemplateProperty, value);
+ }
+
+ public DataTemplateSelector ItemTemplateSelector
+ {
+ get => ((DataTemplateSelector)(GetValue(ItemTemplateSelectorProperty)));
+ set => SetValue(ItemTemplateSelectorProperty, value);
+ }
+
+ public object LoadingContent
+ {
+ get => GetValue(LoadingContentProperty);
+
+ set => SetValue(LoadingContentProperty, value);
+ }
+
+ public Popup Popup { get; set; }
+
+ public IComboSuggestionProvider Provider
+ {
+ get => (IComboSuggestionProvider)GetValue(ProviderProperty);
+
+ set => SetValue(ProviderProperty, value);
+ }
+
+ public object SelectedItem
+ {
+ get => GetValue(SelectedItemProperty);
+
+ set => SetValue(SelectedItemProperty, value);
+ }
+
+ public SelectionAdapter SelectionAdapter { get; set; }
+
+ public string Text
+ {
+ get => (string)GetValue(TextProperty);
+
+ set => SetValue(TextProperty, value);
+ }
+
+ public string Watermark
+ {
+ get => (string)GetValue(WatermarkProperty);
+
+ set => SetValue(WatermarkProperty, value);
+ }
+ public Brush SuggestionBackground
+ {
+ get => (Brush)GetValue(SuggestionBackgroundProperty);
+
+ set => SetValue(SuggestionBackgroundProperty, value);
+ }
+
+ #endregion
+
+ #region "Methods"
+
+ public static void OnSelectedItemChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ AutoCompleteComboBox act = null;
+ act = d as AutoCompleteComboBox;
+ if (act != null)
+ {
+ if (act.Editor != null & !act._isUpdatingText)
+ {
+ act._isUpdatingText = true;
+ act.Editor.Text = act.BindingEvaluator.Evaluate(e.NewValue);
+ act._isUpdatingText = false;
+ }
+ }
+ }
+
+ private void ScrollToSelectedItem()
+ {
+ if (ItemsSelector is ListBox listBox && listBox.SelectedItem != null)
+ listBox.ScrollIntoView(listBox.SelectedItem);
+ }
+
+ public new BindingExpressionBase SetBinding(DependencyProperty dp, BindingBase binding){
+ var res = base.SetBinding(dp, binding);
+ CheckForParentTextBindingChange();
+ return res;
+ }
+ public new BindingExpressionBase SetBinding(DependencyProperty dp, String path) {
+ var res = base.SetBinding(dp, path);
+ CheckForParentTextBindingChange();
+ return res;
+ }
+ public new void ClearValue(DependencyPropertyKey key) {
+ base.ClearValue(key);
+ CheckForParentTextBindingChange();
+ }
+ public new void ClearValue(DependencyProperty dp) {
+ base.ClearValue(dp);
+ CheckForParentTextBindingChange();
+ }
+ private void CheckForParentTextBindingChange(bool force=false) {
+ var CurrentBindingMode = BindingOperations.GetBinding(this, TextProperty)?.UpdateSourceTrigger ?? UpdateSourceTrigger.Default;
+ if (CurrentBindingMode != UpdateSourceTrigger.PropertyChanged)//preventing going any less frequent than property changed
+ CurrentBindingMode = UpdateSourceTrigger.Default;
+
+
+ if (CurrentBindingMode == CurrentTextboxTextBindingUpdateMode && force == false)
+ return;
+ var binding = new Binding {
+ Mode = BindingMode.TwoWay,
+ UpdateSourceTrigger = CurrentBindingMode,
+ Path = new PropertyPath(nameof(Text)),
+ RelativeSource = new RelativeSource(RelativeSourceMode.TemplatedParent),
+ };
+ CurrentTextboxTextBindingUpdateMode = CurrentBindingMode;
+ Editor?.SetBinding(TextBox.TextProperty, binding);
+ }
+
+ private UpdateSourceTrigger CurrentTextboxTextBindingUpdateMode;
+
+ public override void OnApplyTemplate()
+ {
+ base.OnApplyTemplate();
+
+ Editor = Template.FindName(PartEditor, this) as TextBox;
+ Popup = Template.FindName(PartPopup, this) as Popup;
+ ItemsSelector = Template.FindName(PartSelector, this) as Selector;
+ Expander = Template.FindName(PartExpander, this) as Expander;
+
+ BindingEvaluator = new BindingEvaluator(new Binding(DisplayMember));
+
+ if (Editor != null)
+ {
+ Editor.TextChanged += OnEditorTextChanged;
+ Editor.PreviewKeyDown += OnEditorKeyDown;
+ Editor.LostFocus += OnEditorLostFocus;
+ CheckForParentTextBindingChange(true);
+
+ if (SelectedItem != null)
+ {
+ _isUpdatingText = true;
+ Editor.Text = BindingEvaluator.Evaluate(SelectedItem);
+ _isUpdatingText = false;
+ }
+
+ }
+ if (Expander != null)
+ {
+ Expander.IsExpanded = false;
+ Expander.Collapsed += Expander_Expanded;
+ Expander.Expanded += Expander_Expanded;
+ }
+
+ GotFocus += AutoCompleteComboBox_GotFocus;
+
+ if (Popup != null)
+ {
+ Popup.StaysOpen = false;
+ Popup.Opened += OnPopupOpened;
+ Popup.Closed += OnPopupClosed;
+ }
+ if (ItemsSelector != null)
+ {
+ SelectionAdapter = new SelectionAdapter(ItemsSelector);
+ SelectionAdapter.Commit += OnSelectionAdapterCommit;
+ SelectionAdapter.Cancel += OnSelectionAdapterCancel;
+ SelectionAdapter.SelectionChanged += OnSelectionAdapterSelectionChanged;
+ ItemsSelector.PreviewMouseDown += ItemsSelector_PreviewMouseDown;
+ }
+ }
+
+ private void Expander_Expanded(object sender, RoutedEventArgs e)
+ {
+ this.IsDropDownOpen = Expander.IsExpanded;
+ if (!this.IsDropDownOpen)
+ {
+ return;
+ }
+ if (_suggestionsAdapter == null)
+ {
+ _suggestionsAdapter = new SuggestionsAdapter(this);
+ }
+ if (SelectedItem != null || String.IsNullOrWhiteSpace(Editor.Text))
+ _suggestionsAdapter.ShowFullCollection();
+
+ }
+
+ private void ItemsSelector_PreviewMouseDown(object sender, MouseButtonEventArgs e)
+ {
+ if ((e.OriginalSource as FrameworkElement)?.DataContext == null)
+ return;
+ if (!ItemsSelector.Items.Contains(((FrameworkElement)e.OriginalSource)?.DataContext))
+ return;
+ ItemsSelector.SelectedItem = ((FrameworkElement)e.OriginalSource)?.DataContext;
+ OnSelectionAdapterCommit(SelectionAdapter.EventCause.ItemClicked);
+ e.Handled = true;
+ }
+ private void AutoCompleteComboBox_GotFocus(object sender, RoutedEventArgs e)
+ {
+ Editor?.Focus();
+ }
+
+ private string GetDisplayText(object dataItem)
+ {
+ if (BindingEvaluator == null)
+ {
+ BindingEvaluator = new BindingEvaluator(new Binding(DisplayMember));
+ }
+ if (dataItem == null)
+ {
+ return string.Empty;
+ }
+ if (string.IsNullOrEmpty(DisplayMember))
+ {
+ return dataItem.ToString();
+ }
+ return BindingEvaluator.Evaluate(dataItem);
+ }
+
+ private void OnEditorKeyDown(object sender, KeyEventArgs e)
+ {
+ if (SelectionAdapter != null)
+ {
+ if (IsDropDownOpen)
+ SelectionAdapter.HandleKeyDown(e);
+ else
+ IsDropDownOpen = e.Key == Key.Down || e.Key == Key.Up;
+ }
+ }
+
+ private void OnEditorLostFocus(object sender, RoutedEventArgs e)
+ {
+ if (!IsKeyboardFocusWithin)
+ {
+ IsDropDownOpen = false;
+ }
+ }
+
+ private void OnEditorTextChanged(object sender, TextChangedEventArgs e)
+ {
+ Text = Editor.Text;
+ if (_isUpdatingText)
+ return;
+ if (FetchTimer == null)
+ {
+ FetchTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(Delay) };
+ FetchTimer.Tick += OnFetchTimerTick;
+ }
+ FetchTimer.IsEnabled = false;
+ FetchTimer.Stop();
+ SetSelectedItem(null);
+ if (Editor.Text.Length > 0)
+ {
+ FetchTimer.IsEnabled = true;
+ FetchTimer.Start();
+ }
+ else
+ {
+ IsDropDownOpen = false;
+ }
+ }
+
+ private void OnFetchTimerTick(object sender, EventArgs e)
+ {
+ FetchTimer.IsEnabled = false;
+ FetchTimer.Stop();
+ if (Provider != null && ItemsSelector != null)
+ {
+ Filter = Editor.Text;
+ if (_suggestionsAdapter == null)
+ {
+ _suggestionsAdapter = new SuggestionsAdapter(this);
+ }
+ _suggestionsAdapter.GetSuggestions(Filter);
+ }
+ }
+
+ private void OnPopupClosed(object sender, EventArgs e)
+ {
+ if (!_selectionCancelled)
+ {
+ OnSelectionAdapterCommit(SelectionAdapter.EventCause.PopupClosed);
+ }
+ }
+
+ private void OnPopupOpened(object sender, EventArgs e)
+ {
+ _selectionCancelled = false;
+ ItemsSelector.SelectedItem = SelectedItem;
+ }
+
+ public event EventHandler PreSelectionAdapterFinish;
+ private bool PreSelectionEventSomeoneHandled(SelectionAdapter.EventCause cause, bool is_cancel) {
+ if (PreSelectionAdapterFinish == null)
+ return false;
+ var args = new SelectionAdapter.PreSelectionAdapterFinishArgs { cause = cause, is_cancel = is_cancel };
+ PreSelectionAdapterFinish?.Invoke(this, args);
+ return args.handled;
+
+ }
+ private void OnSelectionAdapterCancel(SelectionAdapter.EventCause cause)
+ {
+ if (PreSelectionEventSomeoneHandled(cause, true))
+ return;
+ _isUpdatingText = true;
+ Editor.Text = SelectedItem == null ? Filter : GetDisplayText(SelectedItem);
+ Editor.SelectionStart = Editor.Text.Length;
+ Editor.SelectionLength = 0;
+ _isUpdatingText = false;
+ IsDropDownOpen = false;
+ _selectionCancelled = true;
+ }
+
+ private void OnSelectionAdapterCommit(SelectionAdapter.EventCause cause)
+ {
+ if (PreSelectionEventSomeoneHandled(cause, false))
+ return;
+
+ if (ItemsSelector.SelectedItem != null)
+ {
+ SelectedItem = ItemsSelector.SelectedItem;
+ _isUpdatingText = true;
+ Editor.Text = GetDisplayText(ItemsSelector.SelectedItem);
+ SetSelectedItem(ItemsSelector.SelectedItem);
+ _isUpdatingText = false;
+ IsDropDownOpen = false;
+ }
+ }
+
+ private void OnSelectionAdapterSelectionChanged()
+ {
+ _isUpdatingText = true;
+ Editor.Text = ItemsSelector.SelectedItem == null ? Filter : GetDisplayText(ItemsSelector.SelectedItem);
+ Editor.SelectionStart = Editor.Text.Length;
+ Editor.SelectionLength = 0;
+ ScrollToSelectedItem();
+ _isUpdatingText = false;
+ }
+
+ private void SetSelectedItem(object item)
+ {
+ _isUpdatingText = true;
+ SelectedItem = item;
+ _isUpdatingText = false;
+ }
+ #endregion
+
+ #region "Nested Types"
+
+ private class SuggestionsAdapter
+ {
+
+ #region "Fields"
+
+ private readonly AutoCompleteComboBox _actb;
+
+ private string _filter;
+ #endregion
+
+ #region "Constructors"
+
+ public SuggestionsAdapter(AutoCompleteComboBox actb)
+ {
+ _actb = actb;
+ }
+
+ #endregion
+
+ #region "Methods"
+
+ public void GetSuggestions(string searchText)
+ {
+ _actb.IsLoading = true;
+ // Do not open drop down if control is not focused
+ if (_actb.IsKeyboardFocusWithin)
+ _actb.IsDropDownOpen = true;
+ _actb.ItemsSelector.ItemsSource = null;
+ ParameterizedThreadStart thInfo = GetSuggestionsAsync;
+ Thread th = new Thread(thInfo);
+ _filter = searchText;
+ th.Start(new object[] { searchText, _actb.Provider });
+ }
+ public void ShowFullCollection()
+ {
+ _filter = string.Empty;
+ _actb.IsLoading = true;
+ // Do not open drop down if control is not focused
+ if (_actb.IsKeyboardFocusWithin)
+ _actb.IsDropDownOpen = true;
+ _actb.ItemsSelector.ItemsSource = null;
+ ParameterizedThreadStart thInfo = GetFullCollectionAsync;
+ Thread th = new Thread(thInfo);
+ th.Start(_actb.Provider);
+ }
+
+ private void DisplaySuggestions(IEnumerable suggestions, string filter)
+ {
+ if (_filter != filter)
+ {
+ return;
+ }
+ _actb.IsLoading = false;
+ _actb.ItemsSelector.ItemsSource = suggestions;
+ // Close drop down if there are no items
+ if (_actb.IsDropDownOpen)
+ {
+ _actb.IsDropDownOpen = _actb.ItemsSelector.HasItems;
+ }
+ }
+
+ private void GetSuggestionsAsync(object param)
+ {
+ if (param is object[] args)
+ {
+ string searchText = Convert.ToString(args[0]);
+ if (args[1] is IComboSuggestionProvider provider)
+ {
+ IEnumerable list = provider.GetSuggestions(searchText);
+ _actb.Dispatcher.BeginInvoke(new Action(DisplaySuggestions), DispatcherPriority.Background, list, searchText);
+ }
+ }
+ }
+ private void GetFullCollectionAsync(object param)
+ {
+ if (param is IComboSuggestionProvider provider)
+ {
+ IEnumerable list = provider.GetFullCollection();
+ _actb.Dispatcher.BeginInvoke(new Action(DisplaySuggestions), DispatcherPriority.Background, list, string.Empty);
+ }
+
+ }
+
+ #endregion
+
+ }
+
+ #endregion
+
+ }
+
+}
diff --git a/AutoCompleteTextBox/Editors/AutoCompleteTextBox.cs b/AutoCompleteTextBox/Editors/AutoCompleteTextBox.cs
new file mode 100644
index 0000000..c2f3665
--- /dev/null
+++ b/AutoCompleteTextBox/Editors/AutoCompleteTextBox.cs
@@ -0,0 +1,569 @@
+using System;
+using System.Collections;
+using System.Threading;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Controls.Primitives;
+using System.Windows.Data;
+using System.Windows.Input;
+using System.Windows.Media;
+using System.Windows.Threading;
+
+namespace AutoCompleteTextBox.Editors
+{
+ [TemplatePart(Name = PartEditor, Type = typeof(TextBox))]
+ [TemplatePart(Name = PartPopup, Type = typeof(Popup))]
+ [TemplatePart(Name = PartSelector, Type = typeof(Selector))]
+ public class AutoCompleteTextBox : Control
+ {
+
+ #region "Fields"
+
+ public const string PartEditor = "PART_Editor";
+ public const string PartPopup = "PART_Popup";
+
+ public const string PartSelector = "PART_Selector";
+ public static readonly DependencyProperty DelayProperty = DependencyProperty.Register("Delay", typeof(int), typeof(AutoCompleteTextBox), new FrameworkPropertyMetadata(200));
+ public static readonly DependencyProperty DisplayMemberProperty = DependencyProperty.Register("DisplayMember", typeof(string), typeof(AutoCompleteTextBox), new FrameworkPropertyMetadata(string.Empty));
+ public static readonly DependencyProperty IconPlacementProperty = DependencyProperty.Register("IconPlacement", typeof(IconPlacement), typeof(AutoCompleteTextBox), new FrameworkPropertyMetadata(IconPlacement.Left));
+ public static readonly DependencyProperty IconProperty = DependencyProperty.Register("Icon", typeof(object), typeof(AutoCompleteTextBox), new FrameworkPropertyMetadata(null));
+ public static readonly DependencyProperty IconVisibilityProperty = DependencyProperty.Register("IconVisibility", typeof(Visibility), typeof(AutoCompleteTextBox), new FrameworkPropertyMetadata(Visibility.Visible));
+ public static readonly DependencyProperty IsDropDownOpenProperty = DependencyProperty.Register("IsDropDownOpen", typeof(bool), typeof(AutoCompleteTextBox), new FrameworkPropertyMetadata(false));
+ public static readonly DependencyProperty IsLoadingProperty = DependencyProperty.Register("IsLoading", typeof(bool), typeof(AutoCompleteTextBox), new FrameworkPropertyMetadata(false));
+ public static readonly DependencyProperty IsReadOnlyProperty = DependencyProperty.Register("IsReadOnly", typeof(bool), typeof(AutoCompleteTextBox), new FrameworkPropertyMetadata(false));
+ public static readonly DependencyProperty ItemTemplateProperty = DependencyProperty.Register("ItemTemplate", typeof(DataTemplate), typeof(AutoCompleteTextBox), new FrameworkPropertyMetadata(null));
+ public static readonly DependencyProperty ItemTemplateSelectorProperty = DependencyProperty.Register("ItemTemplateSelector", typeof(DataTemplateSelector), typeof(AutoCompleteTextBox));
+ public static readonly DependencyProperty LoadingContentProperty = DependencyProperty.Register("LoadingContent", typeof(object), typeof(AutoCompleteTextBox), new FrameworkPropertyMetadata(null));
+ public static readonly DependencyProperty ProviderProperty = DependencyProperty.Register("Provider", typeof(ISuggestionProvider), typeof(AutoCompleteTextBox), new FrameworkPropertyMetadata(null));
+ public static readonly DependencyProperty SelectedItemProperty = DependencyProperty.Register("SelectedItem", typeof(object), typeof(AutoCompleteTextBox), new FrameworkPropertyMetadata(null, OnSelectedItemChanged));
+ public static readonly DependencyProperty TextProperty = DependencyProperty.Register("Text", typeof(string), typeof(AutoCompleteTextBox), new FrameworkPropertyMetadata(string.Empty, propertyChangedCallback:null,coerceValueCallback:null, isAnimationProhibited:false, defaultUpdateSourceTrigger: UpdateSourceTrigger.LostFocus, flags: FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
+ public static readonly DependencyProperty FilterProperty = DependencyProperty.Register("Filter", typeof(string), typeof(AutoCompleteTextBox), new FrameworkPropertyMetadata(string.Empty));
+ public static readonly DependencyProperty MaxLengthProperty = DependencyProperty.Register("MaxLength", typeof(int), typeof(AutoCompleteTextBox), new FrameworkPropertyMetadata(0));
+ public static readonly DependencyProperty CharacterCasingProperty = DependencyProperty.Register("CharacterCasing", typeof(CharacterCasing), typeof(AutoCompleteTextBox), new FrameworkPropertyMetadata(CharacterCasing.Normal));
+ public static readonly DependencyProperty MaxPopUpHeightProperty = DependencyProperty.Register("MaxPopUpHeight", typeof(int), typeof(AutoCompleteTextBox), new FrameworkPropertyMetadata(600));
+ public static readonly DependencyProperty MaxPopUpWidthProperty = DependencyProperty.Register("MaxPopUpWidth", typeof(int), typeof(AutoCompleteTextBox), new FrameworkPropertyMetadata(2000));
+
+ public static readonly DependencyProperty WatermarkProperty = DependencyProperty.Register("Watermark", typeof(string), typeof(AutoCompleteTextBox), new FrameworkPropertyMetadata(string.Empty));
+
+ public static readonly DependencyProperty SuggestionBackgroundProperty = DependencyProperty.Register("SuggestionBackground", typeof(Brush), typeof(AutoCompleteTextBox), new FrameworkPropertyMetadata(Brushes.White));
+ private bool _isUpdatingText;
+ private bool _selectionCancelled;
+
+ private SuggestionsAdapter _suggestionsAdapter;
+
+
+ #endregion
+
+ #region "Constructors"
+
+ static AutoCompleteTextBox()
+ {
+ DefaultStyleKeyProperty.OverrideMetadata(typeof(AutoCompleteTextBox), new FrameworkPropertyMetadata(typeof(AutoCompleteTextBox)));
+ FocusableProperty.OverrideMetadata(typeof(AutoCompleteTextBox), new FrameworkPropertyMetadata(true));
+ }
+
+ #endregion
+
+ #region "Properties"
+
+
+ public int MaxPopupHeight
+ {
+ get => (int)GetValue(MaxPopUpHeightProperty);
+ set => SetValue(MaxPopUpHeightProperty, value);
+ }
+ public int MaxPopupWidth
+ {
+ get => (int)GetValue(MaxPopUpWidthProperty);
+ set => SetValue(MaxPopUpWidthProperty, value);
+ }
+
+ public BindingEvaluator BindingEvaluator { get; set; }
+
+ public CharacterCasing CharacterCasing
+ {
+ get => (CharacterCasing)GetValue(CharacterCasingProperty);
+ set => SetValue(CharacterCasingProperty, value);
+ }
+
+ public int MaxLength
+ {
+ get => (int)GetValue(MaxLengthProperty);
+ set => SetValue(MaxLengthProperty, value);
+ }
+
+ public int Delay
+ {
+ get => (int)GetValue(DelayProperty);
+
+ set => SetValue(DelayProperty, value);
+ }
+
+ public string DisplayMember
+ {
+ get => (string)GetValue(DisplayMemberProperty);
+
+ set => SetValue(DisplayMemberProperty, value);
+ }
+
+ public TextBox Editor { get; set; }
+
+ public DispatcherTimer FetchTimer { get; set; }
+
+ public string Filter
+ {
+ get => (string)GetValue(FilterProperty);
+
+ set => SetValue(FilterProperty, value);
+ }
+
+ public object Icon
+ {
+ get => GetValue(IconProperty);
+
+ set => SetValue(IconProperty, value);
+ }
+
+ public IconPlacement IconPlacement
+ {
+ get => (IconPlacement)GetValue(IconPlacementProperty);
+
+ set => SetValue(IconPlacementProperty, value);
+ }
+
+ public Visibility IconVisibility
+ {
+ get => (Visibility)GetValue(IconVisibilityProperty);
+
+ set => SetValue(IconVisibilityProperty, value);
+ }
+
+ public bool IsDropDownOpen
+ {
+ get => (bool)GetValue(IsDropDownOpenProperty);
+
+ set => SetValue(IsDropDownOpenProperty, value);
+ }
+
+ public bool IsLoading
+ {
+ get => (bool)GetValue(IsLoadingProperty);
+
+ set => SetValue(IsLoadingProperty, value);
+ }
+
+ public bool IsReadOnly
+ {
+ get => (bool)GetValue(IsReadOnlyProperty);
+
+ set => SetValue(IsReadOnlyProperty, value);
+ }
+
+ public Selector ItemsSelector { get; set; }
+
+ public DataTemplate ItemTemplate
+ {
+ get => (DataTemplate)GetValue(ItemTemplateProperty);
+
+ set => SetValue(ItemTemplateProperty, value);
+ }
+
+ public DataTemplateSelector ItemTemplateSelector
+ {
+ get => ((DataTemplateSelector)(GetValue(ItemTemplateSelectorProperty)));
+ set => SetValue(ItemTemplateSelectorProperty, value);
+ }
+
+ public object LoadingContent
+ {
+ get => GetValue(LoadingContentProperty);
+
+ set => SetValue(LoadingContentProperty, value);
+ }
+
+ public Popup Popup { get; set; }
+
+ public ISuggestionProvider Provider
+ {
+ get => (ISuggestionProvider)GetValue(ProviderProperty);
+
+ set => SetValue(ProviderProperty, value);
+ }
+
+ public object SelectedItem
+ {
+ get => GetValue(SelectedItemProperty);
+
+ set => SetValue(SelectedItemProperty, value);
+ }
+
+ public SelectionAdapter SelectionAdapter { get; set; }
+
+ public string Text
+ {
+ get => (string)GetValue(TextProperty);
+
+ set => SetValue(TextProperty, value);
+ }
+
+ public string Watermark
+ {
+ get => (string)GetValue(WatermarkProperty);
+
+ set => SetValue(WatermarkProperty, value);
+ }
+ public Brush SuggestionBackground
+ {
+ get => (Brush)GetValue(SuggestionBackgroundProperty);
+
+ set => SetValue(SuggestionBackgroundProperty, value);
+ }
+
+ #endregion
+
+ #region "Methods"
+
+ public static void OnSelectedItemChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ AutoCompleteTextBox act = null;
+ act = d as AutoCompleteTextBox;
+ if (act != null)
+ {
+ if (act.Editor != null & !act._isUpdatingText)
+ {
+ act._isUpdatingText = true;
+ act.Editor.Text = act.BindingEvaluator.Evaluate(e.NewValue);
+ act._isUpdatingText = false;
+ }
+ }
+ }
+
+ private void ScrollToSelectedItem()
+ {
+ if (ItemsSelector is ListBox listBox && listBox.SelectedItem != null)
+ listBox.ScrollIntoView(listBox.SelectedItem);
+ }
+
+ public new BindingExpressionBase SetBinding(DependencyProperty dp, BindingBase binding){
+ var res = base.SetBinding(dp, binding);
+ CheckForParentTextBindingChange();
+ return res;
+ }
+ public new BindingExpressionBase SetBinding(DependencyProperty dp, String path) {
+ var res = base.SetBinding(dp, path);
+ CheckForParentTextBindingChange();
+ return res;
+ }
+ public new void ClearValue(DependencyPropertyKey key) {
+ base.ClearValue(key);
+ CheckForParentTextBindingChange();
+ }
+ public new void ClearValue(DependencyProperty dp) {
+ base.ClearValue(dp);
+ CheckForParentTextBindingChange();
+ }
+ private void CheckForParentTextBindingChange(bool force=false) {
+ var CurrentBindingMode = BindingOperations.GetBinding(this, TextProperty)?.UpdateSourceTrigger ?? UpdateSourceTrigger.Default;
+ if (CurrentBindingMode != UpdateSourceTrigger.PropertyChanged)//preventing going any less frequent than property changed
+ CurrentBindingMode = UpdateSourceTrigger.Default;
+
+
+ if (CurrentBindingMode == CurrentTextboxTextBindingUpdateMode && force == false)
+ return;
+ var binding = new Binding {
+ Mode = BindingMode.TwoWay,
+ UpdateSourceTrigger = CurrentBindingMode,
+ Path = new PropertyPath(nameof(Text)),
+ RelativeSource = new RelativeSource(RelativeSourceMode.TemplatedParent),
+ };
+ CurrentTextboxTextBindingUpdateMode = CurrentBindingMode;
+ Editor?.SetBinding(TextBox.TextProperty, binding);
+ }
+
+ private UpdateSourceTrigger CurrentTextboxTextBindingUpdateMode;
+ public override void OnApplyTemplate()
+ {
+ base.OnApplyTemplate();
+
+ Editor = Template.FindName(PartEditor, this) as TextBox;
+ Editor.Focus();
+ Popup = Template.FindName(PartPopup, this) as Popup;
+ ItemsSelector = Template.FindName(PartSelector, this) as Selector;
+ BindingEvaluator = new BindingEvaluator(new Binding(DisplayMember));
+
+ if (Editor != null)
+ {
+ Editor.TextChanged += OnEditorTextChanged;
+ Editor.PreviewKeyDown += OnEditorKeyDown;
+ Editor.LostFocus += OnEditorLostFocus;
+ CheckForParentTextBindingChange(true);
+
+ if (SelectedItem != null)
+ {
+ _isUpdatingText = true;
+ Editor.Text = BindingEvaluator.Evaluate(SelectedItem);
+ _isUpdatingText = false;
+ }
+
+ }
+
+ GotFocus += AutoCompleteTextBox_GotFocus;
+ GotKeyboardFocus += AutoCompleteTextBox_GotKeyboardFocus;
+
+ if (Popup != null)
+ {
+ Popup.StaysOpen = false;
+ Popup.Opened += OnPopupOpened;
+ Popup.Closed += OnPopupClosed;
+ }
+ if (ItemsSelector != null)
+ {
+ SelectionAdapter = new SelectionAdapter(ItemsSelector);
+ SelectionAdapter.Commit += OnSelectionAdapterCommit;
+ SelectionAdapter.Cancel += OnSelectionAdapterCancel;
+ SelectionAdapter.SelectionChanged += OnSelectionAdapterSelectionChanged;
+ ItemsSelector.PreviewMouseDown += ItemsSelector_PreviewMouseDown;
+ }
+ }
+ private void ItemsSelector_PreviewMouseDown(object sender, MouseButtonEventArgs e)
+ {
+ if ((e.OriginalSource as FrameworkElement)?.DataContext == null)
+ return;
+ if (!ItemsSelector.Items.Contains(((FrameworkElement)e.OriginalSource)?.DataContext))
+ return;
+ ItemsSelector.SelectedItem = ((FrameworkElement)e.OriginalSource)?.DataContext;
+ OnSelectionAdapterCommit(SelectionAdapter.EventCause.MouseDown);
+ e.Handled = true;
+ }
+ private void AutoCompleteTextBox_GotFocus(object sender, RoutedEventArgs e)
+ {
+ Editor?.Focus();
+ }
+ private void AutoCompleteTextBox_GotKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e) {
+ if (e.NewFocus != this)
+ return;
+ if (e.OldFocus == Editor)
+ MoveFocus(new TraversalRequest(FocusNavigationDirection.Previous));
+
+ }
+
+ private string GetDisplayText(object dataItem)
+ {
+ if (BindingEvaluator == null)
+ {
+ BindingEvaluator = new BindingEvaluator(new Binding(DisplayMember));
+ }
+ if (dataItem == null)
+ {
+ return string.Empty;
+ }
+ if (string.IsNullOrEmpty(DisplayMember))
+ {
+ return dataItem.ToString();
+ }
+ return BindingEvaluator.Evaluate(dataItem);
+ }
+
+ private void OnEditorKeyDown(object sender, KeyEventArgs e)
+ {
+ if (SelectionAdapter != null)
+ {
+ if (IsDropDownOpen)
+ SelectionAdapter.HandleKeyDown(e);
+ else
+ IsDropDownOpen = e.Key == Key.Down || e.Key == Key.Up;
+ }
+ }
+
+ private void OnEditorLostFocus(object sender, RoutedEventArgs e)
+ {
+ if (!IsKeyboardFocusWithin)
+ {
+ IsDropDownOpen = false;
+ }
+ }
+
+ private void OnEditorTextChanged(object sender, TextChangedEventArgs e)
+ {
+ Text = Editor.Text;
+ if (_isUpdatingText)
+ return;
+ if (FetchTimer == null)
+ {
+ FetchTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(Delay) };
+ FetchTimer.Tick += OnFetchTimerTick;
+ }
+ FetchTimer.IsEnabled = false;
+ FetchTimer.Stop();
+ SetSelectedItem(null);
+ if (Editor.Text.Length > 0)
+ {
+ FetchTimer.IsEnabled = true;
+ FetchTimer.Start();
+ }
+ else
+ {
+ IsDropDownOpen = false;
+ }
+ }
+
+ private void OnFetchTimerTick(object sender, EventArgs e)
+ {
+ FetchTimer.IsEnabled = false;
+ FetchTimer.Stop();
+ if (Provider != null && ItemsSelector != null)
+ {
+ Filter = Editor.Text;
+ if (_suggestionsAdapter == null)
+ {
+ _suggestionsAdapter = new SuggestionsAdapter(this);
+ }
+ _suggestionsAdapter.GetSuggestions(Filter);
+ }
+ }
+
+ private void OnPopupClosed(object sender, EventArgs e)
+ {
+ if (!_selectionCancelled)
+ {
+ OnSelectionAdapterCommit(SelectionAdapter.EventCause.PopupClosed);
+ }
+ }
+
+ private void OnPopupOpened(object sender, EventArgs e)
+ {
+ _selectionCancelled = false;
+ ItemsSelector.SelectedItem = SelectedItem;
+ }
+
+ private void OnSelectionAdapterCancel(SelectionAdapter.EventCause cause)
+ {
+ if (PreSelectionEventSomeoneHandled(cause, true))
+ return;
+
+ _isUpdatingText = true;
+ Editor.Text = SelectedItem == null ? Filter : GetDisplayText(SelectedItem);
+ Editor.SelectionStart = Editor.Text.Length;
+ Editor.SelectionLength = 0;
+ _isUpdatingText = false;
+ IsDropDownOpen = false;
+ _selectionCancelled = true;
+
+ }
+
+ public event EventHandler PreSelectionAdapterFinish;
+ private bool PreSelectionEventSomeoneHandled(SelectionAdapter.EventCause cause, bool is_cancel) {
+ if (PreSelectionAdapterFinish == null)
+ return false;
+ var args = new SelectionAdapter.PreSelectionAdapterFinishArgs { cause = cause, is_cancel = is_cancel };
+ PreSelectionAdapterFinish?.Invoke(this, args);
+ return args.handled;
+
+ }
+ private void OnSelectionAdapterCommit(SelectionAdapter.EventCause cause)
+ {
+ if (PreSelectionEventSomeoneHandled(cause, false))
+ return;
+
+ if (ItemsSelector.SelectedItem != null)
+ {
+ SelectedItem = ItemsSelector.SelectedItem;
+ _isUpdatingText = true;
+ Editor.Text = GetDisplayText(ItemsSelector.SelectedItem);
+ SetSelectedItem(ItemsSelector.SelectedItem);
+ _isUpdatingText = false;
+ IsDropDownOpen = false;
+ }
+ }
+
+ private void OnSelectionAdapterSelectionChanged()
+ {
+ _isUpdatingText = true;
+ Editor.Text = ItemsSelector.SelectedItem == null ? Filter : GetDisplayText(ItemsSelector.SelectedItem);
+ Editor.SelectionStart = Editor.Text.Length;
+ Editor.SelectionLength = 0;
+ ScrollToSelectedItem();
+ _isUpdatingText = false;
+ }
+
+ private void SetSelectedItem(object item)
+ {
+ _isUpdatingText = true;
+ SelectedItem = item;
+ _isUpdatingText = false;
+ }
+ #endregion
+
+ #region "Nested Types"
+
+ private class SuggestionsAdapter
+ {
+
+ #region "Fields"
+
+ private readonly AutoCompleteTextBox _actb;
+
+ private string _filter;
+ #endregion
+
+ #region "Constructors"
+
+ public SuggestionsAdapter(AutoCompleteTextBox actb)
+ {
+ _actb = actb;
+ }
+
+ #endregion
+
+ #region "Methods"
+
+ public void GetSuggestions(string searchText)
+ {
+ _filter = searchText;
+ _actb.IsLoading = true;
+ // Do not open drop down if control is not focused
+ if (_actb.IsKeyboardFocusWithin)
+ _actb.IsDropDownOpen = true;
+ _actb.ItemsSelector.ItemsSource = null;
+ ParameterizedThreadStart thInfo = GetSuggestionsAsync;
+ Thread th = new Thread(thInfo);
+ th.Start(new object[] { searchText, _actb.Provider });
+ }
+
+ private void DisplaySuggestions(IEnumerable suggestions, string filter)
+ {
+ if (_filter != filter)
+ {
+ return;
+ }
+ _actb.IsLoading = false;
+ _actb.ItemsSelector.ItemsSource = suggestions;
+ // Close drop down if there are no items
+ if (_actb.IsDropDownOpen)
+ {
+ _actb.IsDropDownOpen = _actb.ItemsSelector.HasItems;
+ }
+ }
+
+ private void GetSuggestionsAsync(object param)
+ {
+ if (param is object[] args)
+ {
+ string searchText = Convert.ToString(args[0]);
+ if (args[1] is ISuggestionProvider provider)
+ {
+ IEnumerable list = provider.GetSuggestions(searchText);
+ _actb.Dispatcher.BeginInvoke(new Action(DisplaySuggestions), DispatcherPriority.Background, list, searchText);
+ }
+ }
+ }
+
+ #endregion
+
+ }
+
+ #endregion
+
+ }
+
+}
diff --git a/AutoCompleteTextBox/Editors/IComboSuggestionProvider.cs b/AutoCompleteTextBox/Editors/IComboSuggestionProvider.cs
new file mode 100644
index 0000000..ca95d08
--- /dev/null
+++ b/AutoCompleteTextBox/Editors/IComboSuggestionProvider.cs
@@ -0,0 +1,16 @@
+using System.Collections;
+
+
+namespace AutoCompleteTextBox.Editors
+{
+ public interface IComboSuggestionProvider
+ {
+
+ #region Public Methods
+
+ IEnumerable GetSuggestions(string filter);
+ IEnumerable GetFullCollection();
+
+ #endregion Public Methods
+ }
+}
diff --git a/AutoCompleteTextBox/Editors/ISuggestionProvider.cs b/AutoCompleteTextBox/Editors/ISuggestionProvider.cs
new file mode 100644
index 0000000..7887021
--- /dev/null
+++ b/AutoCompleteTextBox/Editors/ISuggestionProvider.cs
@@ -0,0 +1,15 @@
+using System.Collections;
+
+namespace AutoCompleteTextBox.Editors
+{
+ public interface ISuggestionProvider
+ {
+
+ #region Public Methods
+
+ IEnumerable GetSuggestions(string filter);
+
+ #endregion Public Methods
+
+ }
+}
diff --git a/AutoCompleteTextBox/Editors/SelectionAdapter.cs b/AutoCompleteTextBox/Editors/SelectionAdapter.cs
new file mode 100644
index 0000000..a95f0fc
--- /dev/null
+++ b/AutoCompleteTextBox/Editors/SelectionAdapter.cs
@@ -0,0 +1,122 @@
+using System.Diagnostics;
+using System.Windows.Controls.Primitives;
+using System.Windows.Input;
+
+namespace AutoCompleteTextBox.Editors
+{
+ public class SelectionAdapter
+ {
+ public class PreSelectionAdapterFinishArgs {
+ public EventCause cause;
+ public bool is_cancel;
+ public bool handled;
+ }
+
+ #region "Fields"
+ #endregion
+
+ #region "Constructors"
+
+ public SelectionAdapter(Selector selector)
+ {
+ SelectorControl = selector;
+ SelectorControl.PreviewMouseUp += OnSelectorMouseDown;
+ }
+
+ #endregion
+
+ #region "Events"
+
+ public enum EventCause { Other, PopupClosed, ItemClicked, EnterPressed, EscapePressed, TabPressed, MouseDown}
+ public delegate void CancelEventHandler(EventCause cause);
+
+ public delegate void CommitEventHandler(EventCause cause);
+
+ public delegate void SelectionChangedEventHandler();
+
+ public event CancelEventHandler Cancel;
+ public event CommitEventHandler Commit;
+ public event SelectionChangedEventHandler SelectionChanged;
+ #endregion
+
+ #region "Properties"
+
+ public Selector SelectorControl { get; set; }
+
+ #endregion
+
+ #region "Methods"
+
+ public void HandleKeyDown(KeyEventArgs key)
+ {
+ switch (key.Key)
+ {
+ case Key.Down:
+ IncrementSelection();
+ break;
+ case Key.Up:
+ DecrementSelection();
+ break;
+ case Key.Enter:
+ Commit?.Invoke(EventCause.EnterPressed);
+
+ break;
+ case Key.Escape:
+ Cancel?.Invoke(EventCause.EscapePressed);
+
+ break;
+ case Key.Tab:
+ Commit?.Invoke(EventCause.TabPressed);
+
+ break;
+ default:
+ return;
+ }
+ key.Handled = true;
+ }
+
+ private void DecrementSelection()
+ {
+ if (SelectorControl.SelectedIndex == -1)
+ {
+ SelectorControl.SelectedIndex = SelectorControl.Items.Count - 1;
+ }
+ else
+ {
+ SelectorControl.SelectedIndex -= 1;
+ }
+
+ SelectionChanged?.Invoke();
+ }
+
+ private void IncrementSelection()
+ {
+ if (SelectorControl.SelectedIndex == SelectorControl.Items.Count - 1)
+ {
+ SelectorControl.SelectedIndex = -1;
+ }
+ else
+ {
+ SelectorControl.SelectedIndex += 1;
+ }
+
+ SelectionChanged?.Invoke();
+ }
+
+ private void OnSelectorMouseDown(object sender, MouseButtonEventArgs e)
+ {
+ // If sender is the RepeatButton from the scrollbar we need to
+ // to skip this event otherwise focus get stuck in the RepeatButton
+ // and list is scrolled up or down til the end.
+ if (e.OriginalSource.GetType() != typeof(RepeatButton))
+ {
+ Commit?.Invoke(EventCause.MouseDown);
+ e.Handled = true;
+ }
+ }
+
+ #endregion
+
+ }
+
+}
\ No newline at end of file
diff --git a/AutoCompleteTextBox/Editors/SuggestionProvider.cs b/AutoCompleteTextBox/Editors/SuggestionProvider.cs
new file mode 100644
index 0000000..b63234f
--- /dev/null
+++ b/AutoCompleteTextBox/Editors/SuggestionProvider.cs
@@ -0,0 +1,35 @@
+using System;
+using System.Collections;
+
+namespace AutoCompleteTextBox.Editors
+{
+ public class SuggestionProvider : ISuggestionProvider
+ {
+
+
+ #region Private Fields
+
+ private readonly Func _method;
+
+ #endregion Private Fields
+
+ #region Public Constructors
+
+ public SuggestionProvider(Func method)
+ {
+ _method = method ?? throw new ArgumentNullException(nameof(method));
+ }
+
+ #endregion Public Constructors
+
+ #region Public Methods
+
+ public IEnumerable GetSuggestions(string filter)
+ {
+ return _method(filter);
+ }
+
+ #endregion Public Methods
+
+ }
+}
\ No newline at end of file
diff --git a/AutoCompleteTextBox/Editors/Themes/Generic.xaml b/AutoCompleteTextBox/Editors/Themes/Generic.xaml
new file mode 100644
index 0000000..fe4d0f3
--- /dev/null
+++ b/AutoCompleteTextBox/Editors/Themes/Generic.xaml
@@ -0,0 +1,268 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/AutoCompleteTextBox/Enumerations.cs b/AutoCompleteTextBox/Enumerations.cs
new file mode 100644
index 0000000..d5c0e1a
--- /dev/null
+++ b/AutoCompleteTextBox/Enumerations.cs
@@ -0,0 +1,8 @@
+namespace AutoCompleteTextBox
+{
+ public enum IconPlacement
+ {
+ Left,
+ Right
+ }
+}
\ No newline at end of file
diff --git a/AutoCompleteTextBox/Properties/AssemblyInfo.cs b/AutoCompleteTextBox/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000..93c0d2c
--- /dev/null
+++ b/AutoCompleteTextBox/Properties/AssemblyInfo.cs
@@ -0,0 +1,53 @@
+using System.Reflection;
+using System.Runtime.InteropServices;
+using System.Windows;
+using System.Windows.Markup;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("AutoCompleteTextBox")]
+[assembly: AssemblyDescription("An autocomplete textbox for WPF")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("AutoCompleteTextBox")]
+[assembly: AssemblyCopyright("Copyright © 2019")]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components. If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+//In order to begin building localizable applications, set
+//CultureYouAreCodingWith in your .csproj file
+//inside a . For example, if you are using US english
+//in your source files, set the to en-US. Then uncomment
+//the NeutralResourceLanguage attribute below. Update the "en-US" in
+//the line below to match the UICulture setting in the project file.
+
+//[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)]
+
+
+[assembly:ThemeInfo(
+ ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
+ //(used if a resource is not found in the page,
+ // or application resource dictionaries)
+ ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
+ //(used if a resource is not found in the page,
+ // app, or any theme specific resource dictionaries)
+)]
+
+
+// Version information for an assembly consists of the following four values:
+//
+// Major Version
+// Minor Version
+// Build Number
+// Revision
+//
+// You can specify all the values or you can default the Build and Revision Numbers
+// by using the '*' as shown below:
+// [assembly: AssemblyVersion("1.0.*")]
+[assembly: AssemblyVersion("1.1.1.0")]
+[assembly: AssemblyFileVersion("1.1.1.0")]
+[assembly: XmlnsDefinition("http://wpfcontrols.com/", "AutoCompleteTextBox")]
+[assembly: XmlnsDefinition("http://wpfcontrols.com/", "AutoCompleteTextBox.Editors")]
diff --git a/AutoCompleteTextBox/Properties/Resources.Designer.cs b/AutoCompleteTextBox/Properties/Resources.Designer.cs
new file mode 100644
index 0000000..2d4da73
--- /dev/null
+++ b/AutoCompleteTextBox/Properties/Resources.Designer.cs
@@ -0,0 +1,63 @@
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.42000
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+namespace AutoCompleteTextBox.Properties {
+ using System;
+
+
+ ///
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ ///
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class Resources {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal Resources() {
+ }
+
+ ///
+ /// Returns the cached ResourceManager instance used by this class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("AutoCompleteTextBox.Properties.Resources", typeof(Resources).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ ///
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+ }
+}
diff --git a/AutoCompleteTextBox/Properties/Resources.resx b/AutoCompleteTextBox/Properties/Resources.resx
new file mode 100644
index 0000000..af7dbeb
--- /dev/null
+++ b/AutoCompleteTextBox/Properties/Resources.resx
@@ -0,0 +1,117 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
\ No newline at end of file
diff --git a/AutoCompleteTextBox/Properties/Settings.Designer.cs b/AutoCompleteTextBox/Properties/Settings.Designer.cs
new file mode 100644
index 0000000..5bc23d8
--- /dev/null
+++ b/AutoCompleteTextBox/Properties/Settings.Designer.cs
@@ -0,0 +1,26 @@
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.42000
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+namespace AutoCompleteTextBox.Properties {
+
+
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "16.0.0.0")]
+ internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase {
+
+ private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
+
+ public static Settings Default {
+ get {
+ return defaultInstance;
+ }
+ }
+ }
+}
diff --git a/AutoCompleteTextBox/Properties/Settings.settings b/AutoCompleteTextBox/Properties/Settings.settings
new file mode 100644
index 0000000..033d7a5
--- /dev/null
+++ b/AutoCompleteTextBox/Properties/Settings.settings
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/AutoCompleteTextBox/Themes/Generic.xaml b/AutoCompleteTextBox/Themes/Generic.xaml
new file mode 100644
index 0000000..173cb11
--- /dev/null
+++ b/AutoCompleteTextBox/Themes/Generic.xaml
@@ -0,0 +1,6 @@
+
+
+
+
+
diff --git a/Config.Net/Config.Net.csproj b/Config.Net/Config.Net.csproj
new file mode 100644
index 0000000..2caface
--- /dev/null
+++ b/Config.Net/Config.Net.csproj
@@ -0,0 +1,64 @@
+
+
+ Copyright (c) 2015-2022 by Ivan Gavryliuk
+ Config.Net
+ Ivan Gavryliuk (@aloneguid)
+ Config.Net
+ Config.Net
+ 4.0.0.0
+ 4.7.3.0
+ 4.7.3
+ Super simple configuration framework for .NET focused on developer ergonomics and strong typing. Supports multiple configuration sources such as .ini, .json, .xml files, as well as external providers pluggable by other NuGet packages.
+ true
+
+ Apache-2.0
+
+ enable
+ latest
+ https://github.com/aloneguid/config
+ https://github.com/aloneguid/config
+
+
+
+
+
+
+ netstandard2.0;netstandard2.1;netcoreapp3.1;net5.0;net6.0
+
+
+
+
+ netstandard2.0;netstandard2.1;netcoreapp3.1;net5.0;net6.0;net7.0
+
+
+
+
+
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers
+
+
+
\ No newline at end of file
diff --git a/Config.Net/ConfigurationBuilder.cs b/Config.Net/ConfigurationBuilder.cs
new file mode 100644
index 0000000..a946b58
--- /dev/null
+++ b/Config.Net/ConfigurationBuilder.cs
@@ -0,0 +1,72 @@
+using System;
+using System.Collections.Generic;
+using System.Reflection;
+using System.Text;
+using Castle.DynamicProxy;
+using Config.Net.Core;
+
+namespace Config.Net
+{
+ public class ConfigurationBuilder where T : class
+ {
+ private readonly ProxyGenerator _generator = new ProxyGenerator();
+ private List _stores = new List();
+ private TimeSpan _cacheInterval = TimeSpan.Zero;
+ private readonly List _customParsers = new List();
+
+ public ConfigurationBuilder()
+ {
+ TypeInfo ti = typeof(T).GetTypeInfo();
+
+ if (!ti.IsInterface) throw new ArgumentException($"{ti.FullName} must be an interface", ti.FullName);
+ }
+
+ ///
+ /// Creates an instance of the configuration interface
+ ///
+ ///
+ public T Build()
+ {
+ var valueHandler = new ValueHandler(_customParsers);
+ var ioHandler = new IoHandler(_stores, valueHandler, _cacheInterval);
+
+ T instance = _generator.CreateInterfaceProxyWithoutTarget(new InterfaceInterceptor(typeof(T), ioHandler));
+
+ return instance;
+ }
+
+ ///
+ /// Set to anything different from to add caching for values. By default
+ /// Config.Net doesn't cache any values
+ ///
+ ///
+ ///
+ public ConfigurationBuilder CacheFor(TimeSpan time)
+ {
+ _cacheInterval = time;
+
+ return this;
+ }
+
+ public ConfigurationBuilder UseConfigStore(IConfigStore store)
+ {
+ _stores.Add(store);
+ return this;
+ }
+
+ ///
+ /// Adds a custom type parser
+ ///
+ public ConfigurationBuilder UseTypeParser(ITypeParser parser)
+ {
+ if (parser == null)
+ {
+ throw new ArgumentNullException(nameof(parser));
+ }
+
+ _customParsers.Add(parser);
+
+ return this;
+ }
+ }
+}
diff --git a/Config.Net/ConfigurationExtensions.cs b/Config.Net/ConfigurationExtensions.cs
new file mode 100644
index 0000000..72ab7e0
--- /dev/null
+++ b/Config.Net/ConfigurationExtensions.cs
@@ -0,0 +1,146 @@
+using System.Reflection;
+using Config.Net.Stores;
+using System.Collections.Generic;
+using Config.Net.Stores.Impl.CommandLine;
+
+namespace Config.Net
+{
+ ///
+ /// Configuration extensions
+ ///
+ public static class ConfigurationExtensions
+ {
+ ///
+ /// In-memory dictionary. Optionally you can pass pre-created dictionary, otherwise it will be created internally as empty.
+ ///
+ public static ConfigurationBuilder UseInMemoryDictionary(
+ this ConfigurationBuilder builder,
+ IDictionary? container = null) where TInterface : class
+ {
+ builder.UseConfigStore(new DictionaryConfigStore(container));
+ return builder;
+ }
+
+ ///
+ /// Standard app.config (web.config) builder store. Read-only.
+ ///
+ public static ConfigurationBuilder UseAppConfig(this ConfigurationBuilder builder) where TInterface : class
+ {
+ builder.UseConfigStore(new AppConfigStore());
+ return builder;
+ }
+
+ ///
+ /// Reads builder from the .dll.config or .exe.config file.
+ ///
+ ///
+ /// Reference to the assembly to look for
+ ///
+ public static ConfigurationBuilder UseAssemblyConfig(this ConfigurationBuilder builder, Assembly assembly) where TInterface : class
+ {
+ builder.UseConfigStore(new AssemblyConfigStore(assembly));
+ return builder;
+ }
+
+ ///
+ /// Uses system environment variables
+ ///
+ public static ConfigurationBuilder UseEnvironmentVariables(this ConfigurationBuilder builder) where TInterface : class
+ {
+ builder.UseConfigStore(new EnvironmentVariablesStore());
+ return builder;
+ }
+
+
+ ///
+ /// Simple INI storage.
+ ///
+ ///
+ /// File does not have to exist, however it will be created as soon as you try to write to it.
+ /// When true, inline comments are parsed. It is set to false by default so inline comments are considered a part of the value.
+ ///
+ public static ConfigurationBuilder UseIniFile(this ConfigurationBuilder builder,
+ string iniFilePath,
+ bool parseInlineComments = false) where TInterface : class
+ {
+ builder.UseConfigStore(new IniFileConfigStore(iniFilePath, true, parseInlineComments));
+ return builder;
+ }
+
+ ///
+ /// Simple INI storage.
+ ///
+ ///
+ /// File contents
+ /// When true, inline comments are parsed. It is set to false by default so inline comments are considered a part of the value
+ ///
+ public static ConfigurationBuilder UseIniString(this ConfigurationBuilder builder,
+ string iniString,
+ bool parseInlineComments = false) where TInterface : class
+ {
+ builder.UseConfigStore(new IniFileConfigStore(iniString, false, parseInlineComments));
+ return builder;
+ }
+
+ ///
+ /// Accepts builder from the command line arguments. This is not intended to replace a command line parsing framework but rather
+ /// complement it in a builder like way. Uses current process' command line parameters automatically
+ ///
+ /// Configuration object
+ /// When true argument names are case sensitive, false by default
+ /// Changed builder
+ public static ConfigurationBuilder UseCommandLineArgs(this ConfigurationBuilder builder,
+ bool isCaseSensitive = false,
+ params KeyValuePair[] parameterNameToPosition)
+ where TInterface : class
+ {
+ builder.UseConfigStore(new CommandLineConfigStore(null, isCaseSensitive, parameterNameToPosition));
+ return builder;
+ }
+
+ public static ConfigurationBuilder UseCommandLineArgs(this ConfigurationBuilder builder,
+ bool isCaseSensitive = false,
+ string[]? args = null,
+ params KeyValuePair[] parameterNameToPosition)
+ where TInterface : class
+ {
+ builder.UseConfigStore(new CommandLineConfigStore(args, isCaseSensitive, parameterNameToPosition));
+ return builder;
+ }
+
+ public static ConfigurationBuilder UseCommandLineArgs(this ConfigurationBuilder builder,
+ params KeyValuePair[] parameterNameToPosition)
+ where TInterface : class
+ {
+ builder.UseConfigStore(new CommandLineConfigStore(null, false, parameterNameToPosition));
+ return builder;
+ }
+
+ ///
+ /// Uses JSON file as a builder storage.
+ ///
+ /// Configuration object.
+ /// Full path to json storage file.
+ /// Changed builder.
+ /// Storage file does not have to exist, however it will be created as soon as first write performed.
+ public static ConfigurationBuilder UseJsonFile(this ConfigurationBuilder builder, string jsonFilePath) where TInterface : class
+ {
+ builder.UseConfigStore(new JsonConfigStore(jsonFilePath, true));
+ return builder;
+ }
+
+ ///
+ /// Uses JSON file as a builder storage.
+ ///
+ /// Configuration object.
+ /// Json document.
+ /// Changed builder.
+ /// Storage file does not have to exist, however it will be created as soon as first write performed.
+ public static ConfigurationBuilder UseJsonString(this ConfigurationBuilder builder, string jsonString) where TInterface : class
+ {
+ builder.UseConfigStore(new JsonConfigStore(jsonString, false));
+ return builder;
+ }
+
+ }
+}
diff --git a/Config.Net/Core/Box/BoxFactory.cs b/Config.Net/Core/Box/BoxFactory.cs
new file mode 100644
index 0000000..6e99479
--- /dev/null
+++ b/Config.Net/Core/Box/BoxFactory.cs
@@ -0,0 +1,204 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using System.Reflection;
+
+namespace Config.Net.Core.Box
+{
+ static class BoxFactory
+ {
+ public static Dictionary Discover(Type t, ValueHandler valueHandler, string? basePath)
+ {
+ var result = new Dictionary();
+
+ DiscoverProperties(t, valueHandler, result, basePath);
+
+ DiscoverMethods(t, valueHandler, result);
+
+ return result;
+ }
+
+ private static void DiscoverProperties(Type t, ValueHandler valueHandler, Dictionary result, string? basePath)
+ {
+ IEnumerable properties = GetHierarchyPublicProperties(t);
+
+ foreach (PropertyInfo pi in properties)
+ {
+ Type propertyType = pi.PropertyType;
+ ResultBox rbox;
+ bool isCollection = false;
+
+ if(ResultBox.TryGetCollection(propertyType, out propertyType))
+ {
+ if(pi.SetMethod != null)
+ {
+ throw new NotSupportedException($"Collection properties cannot have a setter. Detected at '{OptionPath.Combine(basePath, pi.Name)}'");
+ }
+
+ isCollection = true;
+ }
+
+ if(propertyType.GetTypeInfo().IsInterface)
+ {
+ rbox = new ProxyResultBox(pi.Name, propertyType);
+ }
+ else
+ {
+ rbox = new PropertyResultBox(pi.Name, propertyType);
+ }
+
+ ValidateSupportedType(rbox, valueHandler);
+
+ AddAttributes(rbox, pi, valueHandler);
+
+ //adjust to collection
+ if(isCollection)
+ {
+ rbox = new CollectionResultBox(pi.Name, rbox);
+ AddAttributes(rbox, pi, valueHandler);
+ }
+
+ result[pi.Name] = rbox;
+ }
+ }
+
+ private static void DiscoverMethods(Type t, ValueHandler valueHandler, Dictionary result)
+ {
+ TypeInfo ti = t.GetTypeInfo();
+
+ IEnumerable methods = ti.DeclaredMethods.Where(m => !m.IsSpecialName);
+
+ foreach (MethodInfo method in methods)
+ {
+ var mbox = new MethodResultBox(method);
+
+ AddAttributes(mbox, method, valueHandler);
+
+ result[mbox.Name] = mbox;
+ }
+ }
+
+ private static void ValidateSupportedType(ResultBox rb, ValueHandler valueHandler)
+ {
+ Type? t = null;
+
+ if (rb is PropertyResultBox pbox)
+ t = rb.ResultBaseType;
+
+ if (t != null && !valueHandler.IsSupported(t))
+ {
+ throw new NotSupportedException($"type {t} on object '{rb.Name}' is not supported.");
+ }
+ }
+
+ private static object? GetDefaultValue(Type t)
+ {
+ if (t.GetTypeInfo().IsValueType) return Activator.CreateInstance(t);
+
+ return null;
+ }
+
+ private static void AddAttributes(ResultBox box, PropertyInfo pi, ValueHandler valueHandler)
+ {
+ AddAttributes(box, valueHandler,
+ pi.GetCustomAttribute(),
+ pi.GetCustomAttribute());
+ }
+
+ private static void AddAttributes(ResultBox box, MethodInfo mi, ValueHandler valueHandler)
+ {
+ AddAttributes(box, valueHandler, mi.GetCustomAttribute(), mi.GetCustomAttribute());
+ }
+
+
+ private static void AddAttributes(ResultBox box, ValueHandler valueHandler, params Attribute?[] attributes)
+ {
+ OptionAttribute? optionAttribute = attributes.OfType().FirstOrDefault();
+ DefaultValueAttribute? defaultValueAttribute = attributes.OfType().FirstOrDefault();
+
+ if (optionAttribute?.Alias != null)
+ {
+ box.StoreByName = optionAttribute.Alias;
+ }
+
+ box.DefaultResult = GetDefaultValue(optionAttribute?.DefaultValue, box, valueHandler) ??
+ GetDefaultValue(defaultValueAttribute?.Value, box, valueHandler) ??
+ GetDefaultValue(box.ResultType);
+ }
+
+ private static object? GetDefaultValue(object? defaultValue, ResultBox box, ValueHandler valueHandler)
+ {
+ object? result = null;
+ if (defaultValue != null)
+ {
+ //validate that types for default value match
+ Type dvt = defaultValue.GetType();
+
+ if (dvt != box.ResultType && dvt != typeof(string))
+ {
+ throw new InvalidCastException($"Default value for option {box.Name} is of type {dvt.FullName} whereas the property has type {box.ResultType.FullName}. To fix this, either set default value to type {box.ResultType.FullName} or a string parseable to the target type.");
+ }
+
+ if (box.ResultType != typeof(string) && dvt == typeof(string))
+ {
+ valueHandler.TryParse(box.ResultType, (string?)defaultValue, out result);
+ }
+ }
+
+ if (result == null)
+ {
+ result = defaultValue;
+ }
+
+ return result;
+ }
+
+ private static PropertyInfo[] GetHierarchyPublicProperties(Type type)
+ {
+ var propertyInfos = new List();
+
+ var considered = new List();
+ var queue = new Queue();
+ considered.Add(type.GetTypeInfo());
+ queue.Enqueue(type.GetTypeInfo());
+
+ while (queue.Count > 0)
+ {
+ TypeInfo typeInfo = queue.Dequeue();
+
+ //add base interfaces to the queue
+ foreach (Type subInterface in typeInfo.ImplementedInterfaces)
+ {
+ TypeInfo subInterfaceTypeInfo = subInterface.GetTypeInfo();
+
+ if (considered.Contains(subInterfaceTypeInfo)) continue;
+
+ considered.Add(subInterfaceTypeInfo);
+ queue.Enqueue(subInterfaceTypeInfo);
+ }
+
+ //add base classes to the queue
+ if (typeInfo.BaseType != null)
+ {
+ TypeInfo baseType = typeInfo.BaseType.GetTypeInfo();
+
+ if (!considered.Contains(baseType))
+ {
+ considered.Add(baseType);
+ queue.Enqueue(baseType);
+ }
+ }
+
+
+ //get properties from the current type
+ IEnumerable newProperties = typeInfo.DeclaredProperties.Where(p => !propertyInfos.Contains(p));
+ propertyInfos.InsertRange(0, newProperties);
+ }
+
+ return propertyInfos.ToArray();
+ }
+
+
+ }
+}
diff --git a/Config.Net/Core/Box/CollectionResultBox.cs b/Config.Net/Core/Box/CollectionResultBox.cs
new file mode 100644
index 0000000..7f2f943
--- /dev/null
+++ b/Config.Net/Core/Box/CollectionResultBox.cs
@@ -0,0 +1,109 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+
+namespace Config.Net.Core.Box
+{
+ class CollectionResultBox : ResultBox
+ {
+ private readonly ResultBox _elementResultBox;
+ private string? _basePath;
+ private DynamicReader? _reader;
+
+ public CollectionResultBox(string name, ResultBox elementBox) : base(name, elementBox.ResultType, null)
+ {
+ _elementResultBox = elementBox;
+ }
+
+ public ResultBox ElementResultBox => _elementResultBox;
+
+ public bool IsInitialised { get; private set; }
+
+ public IEnumerable? CollectionInstance { get; private set; }
+
+ public void Initialise(string? basePath, int length, DynamicReader reader)
+ {
+ _basePath = basePath;
+ _reader = reader;
+
+ CollectionInstance = CreateGenericEnumerable(length);
+
+ IsInitialised = true;
+ }
+
+ private IEnumerable? CreateGenericEnumerable(int count)
+ {
+ Type t = typeof(DynamicEnumerable<>);
+ t = t.MakeGenericType(ResultType);
+
+ IEnumerable? instance = (IEnumerable?)Activator.CreateInstance(t, count, this);
+
+ return instance;
+ }
+
+ private object? ReadAt(int index)
+ {
+ return _reader?.Read(ElementResultBox, index);
+ }
+
+ private class DynamicEnumerable : IEnumerable
+ {
+ private readonly int _count;
+ private readonly CollectionResultBox _parent;
+
+ public DynamicEnumerable(int count, CollectionResultBox parent)
+ {
+ _count = count;
+ _parent = parent;
+ }
+
+ public IEnumerator GetEnumerator()
+ {
+ return new DynamicEnumerator(_count, _parent);
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return new DynamicEnumerator(_count, _parent);
+ }
+ }
+
+ private class DynamicEnumerator : IEnumerator
+ {
+ private int _index = -1;
+ private readonly int _count;
+ private readonly CollectionResultBox _parent;
+ private T? _current;
+
+ public DynamicEnumerator(int count, CollectionResultBox parent)
+ {
+ _count = count;
+ _parent = parent;
+ }
+
+#pragma warning disable CS8603 // Possible null reference return.
+ public T Current => _current ?? default(T);
+
+ object IEnumerator.Current => _current;
+#pragma warning restore CS8603 // Possible null reference return.
+
+ public void Dispose()
+ {
+ }
+
+ public bool MoveNext()
+ {
+ _index += 1;
+
+ _current = (T?)_parent.ReadAt(_index);
+
+ return _index < _count;
+ }
+
+ public void Reset()
+ {
+ _index = -1;
+ }
+ }
+ }
+}
diff --git a/Config.Net/Core/Box/MethodResultBox.cs b/Config.Net/Core/Box/MethodResultBox.cs
new file mode 100644
index 0000000..adf29c3
--- /dev/null
+++ b/Config.Net/Core/Box/MethodResultBox.cs
@@ -0,0 +1,87 @@
+using System;
+using System.Collections.Generic;
+using System.Reflection;
+using System.Text;
+
+namespace Config.Net.Core.Box
+{
+ class MethodResultBox : ResultBox
+ {
+ public MethodResultBox(MethodInfo methodInfo) : base(GetName(methodInfo), GetReturnType(methodInfo), null)
+ {
+ StoreByName = GetStoreName(methodInfo);
+ IsGettter = IsGet(methodInfo);
+ }
+
+ public bool IsGettter { get; private set; }
+
+ ///
+ /// Composes a uniqueue method name using method name itself and parameter type names, separated by underscore
+ ///
+ public static string GetName(MethodInfo mi)
+ {
+ ParameterInfo[] parameters = mi.GetParameters();
+ var sb = new StringBuilder();
+ sb.Append(mi.Name);
+ foreach (ParameterInfo pi in parameters)
+ {
+ sb.Append("-");
+ sb.Append(pi.ParameterType.ToString());
+ }
+ return sb.ToString();
+ }
+
+ public string GetValuePath(object[] arguments)
+ {
+ var sb = new StringBuilder();
+ sb.Append(StoreByName);
+ bool ignoreLast = !IsGettter;
+
+ for (int i = 0; i < arguments.Length - (ignoreLast ? 1 : 0); i++)
+ {
+ object value = arguments[i];
+ if (value == null) continue;
+
+ if (sb.Length > 0)
+ {
+ sb.Append(OptionPath.Separator);
+ }
+ sb.Append(value.ToString());
+ }
+
+ return sb.ToString();
+ }
+
+ private static string GetStoreName(MethodInfo mi)
+ {
+ string name = mi.Name;
+
+ if (name.StartsWith("get", StringComparison.OrdinalIgnoreCase) ||
+ name.StartsWith("set", StringComparison.OrdinalIgnoreCase))
+ {
+ name = name.Substring(3);
+ }
+
+ return name;
+ }
+
+ private static bool IsGet(MethodInfo mi)
+ {
+ return mi.ReturnType != typeof(void);
+ }
+
+ private static Type GetReturnType(MethodInfo mi)
+ {
+ ParameterInfo[] parameters = mi.GetParameters();
+
+ if (parameters == null || parameters.Length == 0)
+ {
+ throw new InvalidOperationException($"method {mi.Name} must have at least one parameter");
+ }
+
+ Type returnType = IsGet(mi) ? mi.ReturnType : parameters[parameters.Length - 1].ParameterType;
+
+ return returnType;
+ }
+ }
+}
diff --git a/Config.Net/Core/Box/PropertyResultBox.cs b/Config.Net/Core/Box/PropertyResultBox.cs
new file mode 100644
index 0000000..0bef2c2
--- /dev/null
+++ b/Config.Net/Core/Box/PropertyResultBox.cs
@@ -0,0 +1,51 @@
+using System;
+using System.Reflection;
+
+namespace Config.Net.Core.Box
+{
+ class PropertyResultBox : ResultBox
+ {
+ public PropertyResultBox(string name, Type resultType) : base(name, resultType, null)
+ {
+ }
+
+ public static bool IsProperty(MethodInfo mi, out bool isGetter, out string? name)
+ {
+ if (mi.Name.StartsWith("get_"))
+ {
+ isGetter = true;
+ name = mi.Name.Substring(4);
+ return true;
+ }
+
+ if (mi.Name.StartsWith("set_"))
+ {
+ isGetter = false;
+ name = mi.Name.Substring(4);
+ return true;
+ }
+
+ isGetter = false;
+ name = null;
+ return false;
+ }
+
+ public static bool IsProperty(MethodInfo mi, out string? name)
+ {
+ if (mi.Name.StartsWith("get_") || mi.Name.StartsWith("set_"))
+ {
+ name = mi.Name.Substring(4);
+ return true;
+ }
+
+ name = null;
+ return false;
+ }
+
+ public static bool IsGetProperty(MethodInfo mi)
+ {
+ return mi.Name.StartsWith("get_");
+ }
+
+ }
+}
diff --git a/Config.Net/Core/Box/ProxyResultBox.cs b/Config.Net/Core/Box/ProxyResultBox.cs
new file mode 100644
index 0000000..cff7c5b
--- /dev/null
+++ b/Config.Net/Core/Box/ProxyResultBox.cs
@@ -0,0 +1,34 @@
+using System;
+using System.Collections.Generic;
+using Castle.DynamicProxy;
+
+namespace Config.Net.Core.Box
+{
+ class ProxyResultBox : ResultBox
+ {
+ private static readonly ProxyGenerator ProxyGenerator = new ProxyGenerator();
+ private readonly Dictionary _indexToProxyInstance = new Dictionary();
+
+ public ProxyResultBox(string name, Type interfaceType) : base(name, interfaceType, null)
+ {
+ }
+
+ public bool IsInitialisedAt(int index)
+ {
+ return _indexToProxyInstance.ContainsKey(index);
+ }
+
+ public object GetInstanceAt(int index)
+ {
+ return _indexToProxyInstance[index];
+ }
+
+ public void InitialiseAt(int index, IoHandler ioHandler, string prefix)
+ {
+ object instance = ProxyGenerator.CreateInterfaceProxyWithoutTarget(ResultBaseType,
+ new InterfaceInterceptor(ResultBaseType, ioHandler, prefix));
+
+ _indexToProxyInstance[index] = instance;
+ }
+ }
+}
diff --git a/Config.Net/Core/Box/ResultBox.cs b/Config.Net/Core/Box/ResultBox.cs
new file mode 100644
index 0000000..bfd96b4
--- /dev/null
+++ b/Config.Net/Core/Box/ResultBox.cs
@@ -0,0 +1,73 @@
+using System;
+using System.Collections.Generic;
+using System.Reflection;
+using System.Text;
+
+namespace Config.Net.Core.Box
+{
+ abstract class ResultBox
+ {
+ private string? _storeByName;
+
+ protected ResultBox(string name, Type resultType, object? defaultResult)
+ {
+ Name = name;
+ ResultType = resultType;
+ ResultBaseType = GetBaseType(resultType);
+ DefaultResult = defaultResult;
+ }
+
+ public string Name { get; }
+
+ public string StoreByName
+ {
+ get => _storeByName ?? Name;
+ set => _storeByName = value;
+ }
+
+ public Type ResultType { get; }
+
+ public Type ResultBaseType { get; }
+
+ public object? DefaultResult { get; set; }
+
+ #region [ Utility Methods ]
+
+ private static Type GetBaseType(Type t)
+ {
+ TypeInfo ti = t.GetTypeInfo();
+ if (ti.IsClass)
+ {
+ return t;
+ }
+ else
+ {
+ if (ti.IsGenericType && ti.GetGenericTypeDefinition() == typeof(Nullable<>))
+ {
+ return ti.GenericTypeArguments[0];
+ }
+ else
+ {
+ return t;
+ }
+ }
+
+ }
+
+ internal static bool TryGetCollection(Type t, out Type elementType)
+ {
+ TypeInfo ti = t.GetTypeInfo();
+
+ if(ti.IsGenericType && ti.GetGenericTypeDefinition() == typeof(IEnumerable<>))
+ {
+ elementType = ti.GenericTypeArguments[0];
+ return true;
+ }
+
+ elementType = t;
+ return false;
+ }
+
+ #endregion
+ }
+}
diff --git a/Config.Net/Core/DynamicReader.cs b/Config.Net/Core/DynamicReader.cs
new file mode 100644
index 0000000..81674c9
--- /dev/null
+++ b/Config.Net/Core/DynamicReader.cs
@@ -0,0 +1,74 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Config.Net.Core.Box;
+
+namespace Config.Net.Core
+{
+ class DynamicReader
+ {
+ private readonly string? _basePath;
+ private readonly IoHandler _ioHandler;
+
+ public DynamicReader(string? basePath, IoHandler ioHandler)
+ {
+ _basePath = basePath;
+ _ioHandler = ioHandler;
+ }
+
+ public object? Read(ResultBox rbox, int index = -1, params object[] arguments)
+ {
+ if (rbox is PropertyResultBox pbox) return ReadProperty(pbox, index);
+
+ if (rbox is ProxyResultBox xbox) return ReadProxy(xbox, index);
+
+ if (rbox is CollectionResultBox cbox) return ReadCollection(cbox, index);
+
+ if (rbox is MethodResultBox mbox) return ReadMethod(mbox, arguments);
+
+ throw new NotImplementedException($"don't know how to read {rbox.GetType()}");
+ }
+
+ private object? ReadProperty(PropertyResultBox pbox, int index)
+ {
+ string path = OptionPath.Combine(index, _basePath, pbox.StoreByName);
+
+ return _ioHandler.Read(pbox.ResultBaseType, path, pbox.DefaultResult);
+ }
+
+ private object ReadProxy(ProxyResultBox xbox, int index)
+ {
+ if (!xbox.IsInitialisedAt(index))
+ {
+ string prefix = OptionPath.Combine(index, _basePath, xbox.StoreByName);
+
+ xbox.InitialiseAt(index, _ioHandler, prefix);
+ }
+
+ return xbox.GetInstanceAt(index);
+ }
+
+ private object? ReadCollection(CollectionResultBox cbox, int index)
+ {
+ string lengthPath = OptionPath.Combine(index, _basePath, cbox.StoreByName);
+ lengthPath = OptionPath.AddLength(lengthPath);
+
+ if (!cbox.IsInitialised)
+ {
+ int length = (int?)_ioHandler.Read(typeof(int), lengthPath, 0) ?? 0;
+
+ cbox.Initialise(_basePath, length, this);
+ }
+
+ return cbox.CollectionInstance;
+ }
+
+ private object? ReadMethod(MethodResultBox mbox, object[] arguments)
+ {
+ string path = mbox.GetValuePath(arguments);
+ path = OptionPath.Combine(_basePath, path);
+
+ return _ioHandler.Read(mbox.ResultBaseType, path, mbox.DefaultResult);
+ }
+ }
+}
diff --git a/Config.Net/Core/DynamicWriter.cs b/Config.Net/Core/DynamicWriter.cs
new file mode 100644
index 0000000..18039a8
--- /dev/null
+++ b/Config.Net/Core/DynamicWriter.cs
@@ -0,0 +1,48 @@
+using System;
+using Config.Net.Core.Box;
+
+namespace Config.Net.Core
+{
+ class DynamicWriter
+ {
+ private readonly string? _basePath;
+ private readonly IoHandler _ioHandler;
+
+ public DynamicWriter(string? basePath, IoHandler ioHandler)
+ {
+ _basePath = basePath;
+ _ioHandler = ioHandler;
+ }
+
+ public void Write(ResultBox rbox, object[] arguments)
+ {
+ if (rbox is PropertyResultBox pbox) WriteProperty(pbox, arguments);
+
+ else if (rbox is MethodResultBox mbox) WriteMethod(mbox, arguments);
+
+ else if (rbox is ProxyResultBox xbox) WriteProxy(xbox, arguments);
+
+ else throw new NotImplementedException($"don't know how to write {rbox.GetType()}");
+ }
+
+ private void WriteProperty(PropertyResultBox pbox, object[] arguments)
+ {
+ string path = OptionPath.Combine(_basePath, pbox.StoreByName);
+
+ _ioHandler.Write(pbox.ResultBaseType, path, arguments[0]);
+ }
+
+ private void WriteMethod(MethodResultBox mbox, object[] arguments)
+ {
+ object value = arguments[arguments.Length - 1];
+ string path = mbox.GetValuePath(arguments);
+
+ _ioHandler.Write(mbox.ResultBaseType, path, value);
+ }
+
+ private void WriteProxy(ProxyResultBox xbox, object[] arguments)
+ {
+ throw new NotSupportedException("cannot assign values to interface properties");
+ }
+ }
+}
diff --git a/Config.Net/Core/Extensions.cs b/Config.Net/Core/Extensions.cs
new file mode 100644
index 0000000..646d92e
--- /dev/null
+++ b/Config.Net/Core/Extensions.cs
@@ -0,0 +1,16 @@
+using System.Collections.Generic;
+
+namespace Config.Net.Core
+{
+ static class Extensions
+ {
+ public static TValue? GetValueOrDefaultInternal(this IDictionary dictionary, TKey key)
+ where TKey: notnull
+ where TValue: class
+ {
+ if (!dictionary.TryGetValue(key, out TValue? value)) return default(TValue);
+
+ return value;
+ }
+ }
+}
diff --git a/Config.Net/Core/FlatArrays.cs b/Config.Net/Core/FlatArrays.cs
new file mode 100644
index 0000000..e22bf56
--- /dev/null
+++ b/Config.Net/Core/FlatArrays.cs
@@ -0,0 +1,58 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Config.Net.Core;
+using Config.Net.TypeParsers;
+
+namespace Config.Net.Core
+{
+ ///
+ /// Helper class to implement flat arrays
+ ///
+ public static class FlatArrays
+ {
+ public static bool IsArrayLength(string? key, Func getValue, out int length)
+ {
+ if (!OptionPath.TryStripLength(key, out key))
+ {
+ length = 0;
+ return false;
+ }
+
+ string? value = key == null ? null : getValue(key);
+ if (value == null)
+ {
+ length = 0;
+ return false;
+ }
+
+ if (!StringArrayParser.TryParse(value, out string[]? ar))
+ {
+ length = 0;
+ return false;
+ }
+
+ length = ar?.Length ?? 0;
+ return true;
+ }
+
+ public static bool IsArrayElement(string? key, Func getValue, out string? value)
+ {
+ if(!OptionPath.TryStripIndex(key, out key, out int index))
+ {
+ value = null;
+ return false;
+ }
+
+ string? arrayString = key == null ? null : getValue(key);
+ if (!StringArrayParser.TryParse(arrayString, out string[]? array) || index >= array?.Length)
+ {
+ value = null;
+ return false;
+ }
+
+ value = array?[index] ?? null;
+ return true;
+ }
+ }
+}
diff --git a/Config.Net/Core/InterfaceInterceptor.cs b/Config.Net/Core/InterfaceInterceptor.cs
new file mode 100644
index 0000000..b02d3b3
--- /dev/null
+++ b/Config.Net/Core/InterfaceInterceptor.cs
@@ -0,0 +1,103 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Reflection;
+using System.Text;
+using Castle.DynamicProxy;
+using Config.Net.Core.Box;
+
+namespace Config.Net.Core
+{
+ class InterfaceInterceptor : IInterceptor
+ {
+ private readonly Dictionary _boxes;
+ private IoHandler _ioHandler;
+ private readonly string? _prefix;
+ private readonly DynamicReader _reader;
+ private readonly DynamicWriter _writer;
+ private readonly bool _isInpc;
+ private PropertyChangedEventHandler? _inpcHandler;
+
+ public InterfaceInterceptor(Type interfaceType, IoHandler ioHandler, string? prefix = null)
+ {
+ _boxes = BoxFactory.Discover(interfaceType, ioHandler.ValueHandler, prefix);
+ _ioHandler = ioHandler;
+ _prefix = prefix;
+ _reader = new DynamicReader(prefix, ioHandler);
+ _writer = new DynamicWriter(prefix, ioHandler);
+ _isInpc = interfaceType.GetInterface(nameof(INotifyPropertyChanged)) != null;
+ }
+
+ private ResultBox FindBox(IInvocation invocation)
+ {
+ if (PropertyResultBox.IsProperty(invocation.Method, out string? propertyName) && propertyName != null)
+ {
+ return _boxes[propertyName];
+ }
+ else //method
+ {
+ string name = MethodResultBox.GetName(invocation.Method);
+ return _boxes[name];
+ }
+ }
+
+ public void Intercept(IInvocation invocation)
+ {
+ if (TryInterceptInpc(invocation)) return;
+
+ ResultBox rbox = FindBox(invocation);
+
+ bool isRead =
+ (rbox is PropertyResultBox && PropertyResultBox.IsGetProperty(invocation.Method)) ||
+ (rbox is ProxyResultBox && PropertyResultBox.IsGetProperty(invocation.Method)) ||
+ (rbox is MethodResultBox mbox && mbox.IsGettter) ||
+ (rbox is CollectionResultBox);
+
+ if(isRead)
+ {
+ invocation.ReturnValue = _reader.Read(rbox, -1, invocation.Arguments);
+ return;
+ }
+ else
+ {
+ _writer.Write(rbox, invocation.Arguments);
+
+ TryNotifyInpc(invocation, rbox);
+ }
+ }
+
+ private bool TryInterceptInpc(IInvocation invocation)
+ {
+ if (!_isInpc) return false;
+
+ if (invocation.Method.Name == "add_PropertyChanged")
+ {
+ invocation.ReturnValue =
+ _inpcHandler =
+ (PropertyChangedEventHandler)Delegate.Combine(_inpcHandler, (Delegate)invocation.Arguments[0]);
+ return true;
+ }
+ else if(invocation.Method.Name == "remove_PropertyChanged")
+ {
+ invocation.ReturnValue =
+ _inpcHandler =
+ (PropertyChangedEventHandler?)Delegate.Remove(_inpcHandler, (Delegate)invocation.Arguments[0]);
+ return true;
+ }
+
+ return false;
+ }
+
+ private void TryNotifyInpc(IInvocation invocation, ResultBox rbox)
+ {
+ if (_inpcHandler == null || rbox is MethodResultBox) return;
+
+ _inpcHandler.Invoke(invocation.InvocationTarget, new PropertyChangedEventArgs(rbox.Name));
+ if(rbox.Name != rbox.StoreByName)
+ {
+ //notify on StoreByName as well
+ _inpcHandler.Invoke(invocation.InvocationTarget, new PropertyChangedEventArgs(rbox.StoreByName));
+ }
+ }
+ }
+}
diff --git a/Config.Net/Core/IoHandler.cs b/Config.Net/Core/IoHandler.cs
new file mode 100644
index 0000000..0d8e0e2
--- /dev/null
+++ b/Config.Net/Core/IoHandler.cs
@@ -0,0 +1,70 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Config.Net.Core
+{
+ class IoHandler
+ {
+ private readonly IEnumerable _stores;
+ private readonly ValueHandler _valueHandler;
+ private readonly TimeSpan _cacheInterval;
+ private readonly ConcurrentDictionary> _keyToValue = new ConcurrentDictionary>();
+
+ public IoHandler(IEnumerable stores, ValueHandler valueHandler, TimeSpan cacheInterval)
+ {
+ _stores = stores ?? throw new ArgumentNullException(nameof(stores));
+ _valueHandler = valueHandler ?? throw new ArgumentNullException(nameof(valueHandler));
+ _cacheInterval = cacheInterval;
+ }
+
+ public ValueHandler ValueHandler => _valueHandler;
+
+ public object? Read(Type baseType, string path, object? defaultValue)
+ {
+ if(!_keyToValue.TryGetValue(path, out _))
+ {
+ var v = new LazyVar