//******************************************************************************************************
// Adapters.cs - Gbtc
//
// Copyright © 2012, Grid Protection Alliance. All Rights Reserved.
//
// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See
// the NOTICE file distributed with this work for additional information regarding copyright ownership.
// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may
// not use this file except in compliance with the License. You may obtain a copy of the License at:
//
// http://www.opensource.org/licenses/MIT
//
// Unless agreed to in writing, the subject software distributed under the License is distributed on an
// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the
// License for the specific language governing permissions and limitations.
//
// Code Modification History:
// ----------------------------------------------------------------------------------------------------
// 05/05/2011 - Mehulbhai P Thakkar
// Generated original version of source code.
// 12/20/2012 - Starlynn Danyelle Gilliam
// Modified Header.
//
//******************************************************************************************************
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using System.Windows;
using System.Windows.Input;
using GSF.IO;
using GSF.Reflection;
using GSF.TimeSeries.Adapters;
using GSF.TimeSeries.UI.Commands;
using GSF.TimeSeries.UI.DataModels;
// ReSharper disable VirtualMemberCallInConstructor
namespace GSF.TimeSeries.UI.ViewModels
{
///
/// Class to hold bindable collection and current selection information for UI.
///
internal class Adapters : PagedViewModelBase
{
#region [ Members ]
// Nested Types
public class AdapterTypeDescription
{
public string Header { get; set; }
public string Description { get; set; }
public Visibility HeaderVisibility =>
Header is null ? Visibility.Collapsed : Visibility.Visible;
public override string ToString() =>
Header is null ? Description : $"{Header}: {Description}";
}
// Fields
private List> m_adapterTypeList;
private List m_parameterList;
private readonly AdapterType m_adapterType;
private string m_searchDirectory;
private AdapterConnectionStringParameter m_selectedParameter;
private RelayCommand m_initializeCommand;
private string m_runtimeID;
private bool m_suppressParameterValueUpdates;
private bool m_suppressConnectionStringUpdates;
#endregion
#region [ Constructor ]
///
/// Creates an instance of class.
///
/// Integer value to determine number of items per page.
/// Boolean value to determine is user changes should be saved automatically.
/// to determine type.
public Adapters(int itemsPerPage, AdapterType adapterType, bool autoSave = true)
: base(0, autoSave) // Set items per page to zero to avoid load in the base class.
{
AppDomain.CurrentDomain.AssemblyResolve += AppDomain_AssemblyResolve;
ItemsPerPage = itemsPerPage;
// Must attach to PropertyChanged event after setting m_adapterType!
// Do not override OnPropertyChanged or you will regret it!
m_adapterType = adapterType;
PropertyChanged += Adapters_PropertyChanged;
SearchDirectory = FilePath.GetAbsolutePath("").EnsureEnd(Path.DirectorySeparatorChar);
Load();
}
#endregion
#region [ Properties ]
public string RuntimeID
{
get => m_runtimeID;
set
{
m_runtimeID = value;
OnPropertyChanged("RuntimeID");
}
}
///
/// Gets flag that determines if is a new record.
///
public override bool IsNewRecord => CurrentItem.ID == 0;
///
/// Gets or sets the collection containing the adapter types found
/// in the assemblies residing in the .
///
public List> AdapterTypeList
{
get => m_adapterTypeList;
set
{
m_adapterTypeList = value;
OnPropertyChanged("AdapterTypeList");
}
}
///
/// Gets or sets the index of the selected item in the .
///
public int AdapterTypeSelectedIndex
{
get
{
int index = m_adapterTypeList
.Select(tuple => tuple.Item1)
.TakeWhile(type => type.FullName != CurrentItem.TypeName)
.Count();
if (index == m_adapterTypeList.Count)
index = -1;
return index;
}
set
{
if (value >= 0 && value < AdapterTypeList.Count)
CurrentItem.TypeName = AdapterTypeList[value].Item1.FullName;
OnAdapterTypeSelectedIndexChanged();
}
}
///
/// Gets additional information about the type selected from the drop-down.
///
public string TypeInfo => string.IsNullOrEmpty(CurrentItem.TypeName) ? "Adapter Type" :
$"Adapter Type: {CurrentItem.TypeName} from {Path.GetFileName(CurrentItem.AssemblyName)}";
///
/// Gets or sets the list of parameters found by
/// searching the class defined by and
/// plus the parameters defined by the ConnectionString.
///
public List ParameterList
{
get => m_parameterList;
set
{
try
{
// Attempting to update parameter values without suppressing connection
// string updates would result in an infinite loop. We should not be modifying
// the connection string directly at the same time the user is, anyway.
m_suppressConnectionStringUpdates = true;
// Getting this ahead of time allows us to reselect the previously
// selected parameter if it still exists in the parameter list.
AdapterConnectionStringParameter selectedParameter = m_selectedParameter;
m_parameterList = value;
OnPropertyChanged("ParameterList");
// Reselect the previously selected parameter, and update
// the connection string parameters if necessary.
UpdateConnectionStringParameters(m_parameterList, CurrentItem.ConnectionString.ToNonNullString().ParseKeyValuePairs());
SelectedParameter = selectedParameter;
}
finally
{
// Indicate that we want to stop suppressing connection string updates.
m_suppressConnectionStringUpdates = false;
}
}
}
///
/// Gets or sets the directory in which the application will search for
/// assemblies containing adapters. The resulting list of adapters populates
/// the .
///
public string SearchDirectory
{
get => m_searchDirectory;
set
{
if (m_searchDirectory?.Equals(value, StringComparison.OrdinalIgnoreCase) ?? false)
return;
m_searchDirectory = value;
OnPropertyChanged("SearchDirectory");
if (string.IsNullOrEmpty(m_searchDirectory) || Directory.Exists(m_searchDirectory))
{
// When the adapter type list is updated, the selected type will change.
// Since that is bound to the type name which in turn updates the
// assembly name, we need to store the current values ahead of time
// and restore them after updating the adapter type list.
string assemblyName = CurrentItem.AssemblyName;
string typeName = CurrentItem.TypeName;
string ext = string.IsNullOrEmpty(m_searchDirectory) ? string.Empty : Path.DirectorySeparatorChar.ToString();
AdapterTypeList = GetAdapterTypeList(m_searchDirectory + ext, GetAdapterInterfaceType(m_adapterType));
CurrentItem.TypeName = typeName;
CurrentItem.AssemblyName = assemblyName;
}
}
}
///
/// Gets or sets the currently selected parameter from the
/// . This is used to reselect previously
/// selected parameters when the ParameterList is updated.
///
public AdapterConnectionStringParameter SelectedParameter
{
get => m_selectedParameter;
set
{
if (m_selectedParameter is not null)
m_selectedParameter.PropertyChanged -= ConnectionStringParameter_PropertyChanged;
m_selectedParameter = value;
if (m_selectedParameter is not null)
m_selectedParameter.PropertyChanged += ConnectionStringParameter_PropertyChanged;
OnPropertyChanged("SelectedParameter");
}
}
///
/// Gets the command whose action is evoked when the user clicks the initialize button.
///
public ICommand InitializeCommand => m_initializeCommand ?? (m_initializeCommand = new RelayCommand(InitializeAdapter, () => CanSave));
///
/// Determines whether the custom configuration button is visible.
///
public Visibility CustomConfigurationButtonVisibility => AdapterTypeSelectedIndex >= 0 &&
AdapterTypeList[AdapterTypeSelectedIndex].Item1.TryGetAttribute(out CustomConfigurationEditorAttribute _) ?
Visibility.Visible :
Visibility.Collapsed;
#endregion
#region [ Methods ]
///
/// Gets the primary key value of the .
///
/// The primary key value of the .
public override int GetCurrentItemKey() =>
CurrentItem.ID;
///
/// Gets the string based named identifier of the .
///
/// The string based named identifier of the .
public override string GetCurrentItemName() =>
CurrentItem.AdapterName;
///
/// Creates a new instance of and assigns it to CurrentItem.
///
public override void Clear()
{
base.Clear();
CurrentItem.Type = m_adapterType;
ParameterList = null;
}
///
/// Loads collection of defined in the database.
///
public override void Load()
{
Mouse.OverrideCursor = Cursors.Wait;
try
{
if (ItemsKeys is null)
{
ItemsKeys = Adapter.LoadIDs(null, m_adapterType, SortMember, SortDirection);
if (SortSelector is not null)
{
ItemsKeys = SortDirection == "ASC" ?
ItemsKeys.OrderBy(SortSelector).ToList() :
ItemsKeys.OrderByDescending(SortSelector).ToList();
}
}
List pageKeys = ItemsKeys.Skip((CurrentPageNumber - 1) * ItemsPerPage).Take(ItemsPerPage).ToList();
ItemsSource = Adapter.Load(null, m_adapterType, pageKeys);
CurrentItem.Type = m_adapterType;
}
catch (Exception ex)
{
if (ex.InnerException is null)
{
Popup(ex.Message, $"Load {DataModelName} Exception:", MessageBoxImage.Error);
CommonFunctions.LogException(null, $"Load {DataModelName}", ex);
}
else
{
Popup($"{ex.Message}{Environment.NewLine}Inner Exception: {ex.InnerException.Message}", $"Load {DataModelName} Exception:", MessageBoxImage.Error);
CommonFunctions.LogException(null, $"Load {DataModelName}", ex.InnerException);
}
}
finally
{
Mouse.OverrideCursor = null;
}
}
///
/// Deletes associated record.
///
public override void Delete()
{
if (!CanDelete || !Confirm($"Are you sure you want to delete '{GetCurrentItemName()}'?", $"Delete {DataModelName}"))
return;
try
{
if (OnBeforeDeleteCanceled())
throw new OperationCanceledException("Delete was canceled.");
int currentItemKey = GetCurrentItemKey();
string result = Adapter.Delete(null, m_adapterType, GetCurrentItemKey());
ItemsKeys.Remove(currentItemKey);
OnDeleted();
Load();
DisplayStatusMessage(result);
}
catch (Exception ex)
{
if (ex.InnerException is null)
{
Popup(ex.Message, $"Delete {DataModelName} Exception:", MessageBoxImage.Error);
CommonFunctions.LogException(null, $"Delete {DataModelName}", ex);
}
else
{
Popup($"{ex.Message}{Environment.NewLine}Inner Exception: {ex.InnerException.Message}", $"Delete {DataModelName} Exception:", MessageBoxImage.Error);
CommonFunctions.LogException(null, $"Delete {DataModelName}", ex.InnerException);
}
}
}
///
/// Unloads the class.
///
public void Unload() =>
AppDomain.CurrentDomain.AssemblyResolve -= AppDomain_AssemblyResolve;
///
/// Handles PropertyChanged event on CurrentItem.
///
/// Source of the event.
/// Event arguments.
protected override void m_currentItem_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
base.m_currentItem_PropertyChanged(sender, e);
switch (e.PropertyName)
{
// Occurs when CurrentItem.TypeName changes.
case "TypeName":
{
// If there exists a type in the adapter type list that
// matches the type name, also update the assembly name.
Type selectedType = m_adapterTypeList
.Select(tuple => tuple.Item1)
.SingleOrDefault(type => type.FullName == CurrentItem.TypeName);
if (selectedType is not null)
{
string pattern = Regex.Escape(FilePath.GetAbsolutePath("").EnsureEnd(Path.DirectorySeparatorChar));
CurrentItem.AssemblyName = Regex.Replace(selectedType.Assembly.Location, $"^{pattern}", "", RegexOptions.IgnoreCase);
}
// Also update the parameter list. Since the type has changed,
// the current list of parameters generated by searching the
// selected type is no longer valid.
ParameterList = GetParameterList(CurrentItem.AssemblyName, CurrentItem.TypeName);
OnAdapterTypeSelectedIndexChanged();
OnPropertyChanged("TypeInfo");
break;
}
// Occurs when CurrentItem.AssemblyName changes.
case "AssemblyName":
// Occurs when CurrentItem.ConnectionString changes.
// When the connection string changes, we need to keep all related
// elements synchronized. Since we can't easily determine exactly
// what has changed, simply update everything.
case "ConnectionString":
// When the assembly name changes, we may be able to
// find connection string parameters in the class defined
// by the assembly name and type name.
ParameterList = GetParameterList(CurrentItem.AssemblyName, CurrentItem.TypeName);
break;
}
}
///
/// Handles PropertyChanged event on .
///
/// Source of the event.
/// Event arguments.
protected virtual void ConnectionStringParameter_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (m_suppressConnectionStringUpdates || sender is not AdapterConnectionStringParameter parameter || parameter.Value is null || e.PropertyName != "Value")
return;
try
{
m_suppressParameterValueUpdates = true;
// The easiest way to update is to break the connection string into key
// value pairs, update the value of the pair corresponding to the parameter
// that fired the event, and then rejoin the key value pairs.
Dictionary settings = CurrentItem.ConnectionString.ToNonNullString().ParseKeyValuePairs();
settings[parameter.Name] = parameter.Value.ToString();
// Build the new connection string and validate that it can still be parsed
string connectionString = settings.JoinKeyValuePairs();
connectionString.ParseKeyValuePairs();
CurrentItem.ConnectionString = connectionString;
}
finally
{
m_suppressParameterValueUpdates = false;
}
}
private void Adapters_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName != "CurrentItem")
return;
if (CurrentItem is null)
{
RuntimeID = string.Empty;
}
else
{
switch (m_adapterType)
{
case AdapterType.Action:
RuntimeID = CommonFunctions.GetRuntimeID("CustomActionAdapter", CurrentItem.ID);
break;
case AdapterType.Filter:
RuntimeID = CommonFunctions.GetRuntimeID("CustomFilterAdapter", CurrentItem.ID);
break;
case AdapterType.Input:
RuntimeID = CommonFunctions.GetRuntimeID("CustomInputAdapter", CurrentItem.ID);
break;
case AdapterType.Output:
RuntimeID = CommonFunctions.GetRuntimeID("CustomOutputAdapter", CurrentItem.ID);
break;
default:
RuntimeID = string.Empty;
break;
}
// Modify search path to the path of the assembly of the selected item
SearchDirectory = FilePath.GetDirectoryName(FilePath.GetAbsolutePath(CurrentItem.AssemblyName ?? ""));
// If the current item changes, but the connection string does not, the
// parameter list and parameters won't be updated. We take care of that here.
ParameterList = GetParameterList(CurrentItem.AssemblyName, CurrentItem.TypeName);
}
OnAdapterTypeSelectedIndexChanged();
OnPropertyChanged("TypeInfo");
}
private void InitializeAdapter()
{
try
{
if (Confirm($"Do you want to Initialize {GetCurrentItemName()}?", "Confirm Initialize"))
Popup(CommonFunctions.SendCommandToService($"Initialize {RuntimeID}"), "Initialize", MessageBoxImage.Information);
}
catch (Exception ex)
{
Popup($"ERROR: {ex.Message}", "Failed To Initialize", MessageBoxImage.Error);
}
}
///
/// Gets the interface type of the adapter based on
/// the given .
///
/// The adapter type.
/// The interface type corresponding to the adapter type.
private Type GetAdapterInterfaceType(AdapterType type)
{
switch (type)
{
case AdapterType.Filter:
return typeof(IFilterAdapter);
case AdapterType.Input:
return typeof(IInputAdapter);
case AdapterType.Action:
return typeof(IActionAdapter);
case AdapterType.Output:
return typeof(IOutputAdapter);
default:
return null;
}
}
///
/// Searches the for assemblies containing
/// implementations of the given adapter type. It then returns a collection
/// containing all the implementations it found as well as their descriptions.
///
/// The directory in which to search for assemblies.
/// The type to be searched for in the assemblies.
/// The collection of types found as well as their descriptions.
private List> GetAdapterTypeList(string searchDirectory, Type adapterType)
{
return adapterType
.LoadImplementations(searchDirectory, true, false, true)
.Distinct()
.Where(type => GetEditorBrowsableState(type) == EditorBrowsableState.Always)
.Select(type => Tuple.Create(type, GetDescription(type)))
.OrderByDescending(pair => pair.Item1.TryGetAttribute(out DescriptionAttribute _))
.ThenBy(pair => pair.Item2.ToString())
.ToList();
}
///
/// Gets the editor browsable state of the given type. This method will
/// search for a using reflection.
/// If none is found, it will default to .
///
/// The type for which an editor browsable state is found.
///
/// Either the editor browsable state as defined by an
/// or else .
///
private EditorBrowsableState GetEditorBrowsableState(Type type) =>
type.TryGetAttribute(out EditorBrowsableAttribute editorBrowsableAttribute) ?
editorBrowsableAttribute.State :
EditorBrowsableState.Always;
///
/// Gets a description of the given type. This method will search for a
/// using reflection. If none is found,
/// it will default to the of the type.
///
/// The type for which a description is found.
///
/// Either the description as defined by a
/// or else the of the parameter.
///
private static AdapterTypeDescription GetDescription(Type type)
{
AdapterTypeDescription adapterTypeDescription = new AdapterTypeDescription();
string[] splitDescription = type.TryGetAttribute(out DescriptionAttribute descriptionAttribute) ?
descriptionAttribute.Description.ToNonNullNorEmptyString(type.FullName).Split(':') :
new[] { type.FullName };
if (splitDescription.Length > 1)
{
adapterTypeDescription.Header = splitDescription[0].Trim();
adapterTypeDescription.Description = splitDescription[1].Trim();
}
else
{
adapterTypeDescription.Description = splitDescription[0].Trim();
}
return adapterTypeDescription;
}
///
/// Gets the list of connection string parameters found in the assembly
/// defined by the given assembly name and type name. Also included in
/// the list are the keys defined in the
/// which do not match parameters found in the aforementioned type.
///
/// The name of the assembly in which the type resides.
/// The name of the type to be found in the assembly.
///
/// The list of connection string parameters found by
/// searching the given type as well as the connection string.
///
private List GetParameterList(string assemblyName, string typeName)
{
if (assemblyName is not null && typeName is not null)
{
// For convenience, start by searching the type
// list for a type matching the parameters.
Type adapterType = m_adapterTypeList
.Select(tuple => tuple.Item1)
.Where(type => type.Assembly.Location.Equals(FilePath.GetAbsolutePath(assemblyName), StringComparison.OrdinalIgnoreCase))
.SingleOrDefault(type => type.FullName == typeName);
// Attempt to find that assembly and retrieve the type.
if (adapterType is null && File.Exists(assemblyName))
adapterType = Assembly.LoadFrom(assemblyName).GetType(typeName);
if (adapterType is not null)
{
// Get the list of properties with ConnectionStringParameterAttribute annotations.
IEnumerable infoList = adapterType.GetProperties()
.Where(info => info.TryGetAttribute(typeof(ConnectionStringParameterAttribute).FullName, out Attribute _));
// Get the list of connection string keys which do not match
// the names of the properties in the previously obtained list.
IEnumerable keyList = CurrentItem.ConnectionString
.ToNonNullString().ParseKeyValuePairs().Keys
.Where(key => !infoList.Any(info => info.Name.Equals(key, StringComparison.OrdinalIgnoreCase)));
// Convert both lists into ConnectionStringParameter lists, combine
// the two lists, and then order them lexically while giving precedence
// to "required" parameters (those lacking a default value).
return infoList.Select(GetParameter)
.Union(keyList.Select(GetParameter))
.OrderByDescending(parameter => parameter.IsRequired)
.ThenBy(parameter => parameter.Name)
.ToList();
}
}
// If the adapter type could not be found, then we
// can only search the connection string for parameters.
return CurrentItem.ConnectionString.ToNonNullString().ParseKeyValuePairs()
.Select(pair => GetParameter(pair.Key))
.OrderBy(parameter => parameter.Name)
.ToList();
}
///
/// Retrieves an existing parameter from the . If no
/// parameter exists with a name that matches the given ,
/// then a new one is created. There is also a special case in which the parameter is
/// already defined, but no exists for it. In that case,
/// the property info is added, as well as any other new information, and the parameter
/// is returned.
///
/// The property info that defines the connection string parameter.
/// The parameter defined by the given property info.
private AdapterConnectionStringParameter GetParameter(PropertyInfo info)
{
AdapterConnectionStringParameter parameter = null;
bool isRequired = false;
object defaultValue = null;
string description = null;
if (m_parameterList is not null)
parameter = m_parameterList.SingleOrDefault(param => param.Name.Equals(info.Name, StringComparison.OrdinalIgnoreCase));
// These are different cases, but we need to extract the description
// and default value in both. In cases where we already know this
// information, we can skip this step.
if (parameter?.Info is null)
{
isRequired = !info.TryGetAttribute(out DefaultValueAttribute defaultValueAttribute);
defaultValue = isRequired ? null : defaultValueAttribute.Value;
description = info.TryGetAttribute(out DescriptionAttribute descriptionAttribute) ? descriptionAttribute.Description : string.Empty;
}
if (parameter is null)
{
// Create a brand new parameter to be returned.
parameter = new AdapterConnectionStringParameter
{
Info = info,
Name = info.Name,
Description = description,
Value = null,
DefaultValue = defaultValue,
IsRequired = isRequired
};
}
else if (parameter.Info is null)
{
// Update the existing parameter with newly obtained information.
parameter.Info = info;
parameter.Description = description;
parameter.DefaultValue = defaultValue;
}
return parameter;
}
///
/// Retrieves an existing parameter from the . If no
/// parameter exists with a name that matches the one given, then a new one is created.
///
/// The name of the parameter to be retrieved or created.
/// The parameter with the given name.
private AdapterConnectionStringParameter GetParameter(string name)
{
AdapterConnectionStringParameter parameter = null;
if (m_parameterList is not null)
parameter = m_parameterList.SingleOrDefault(param => param.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
return parameter ?? new AdapterConnectionStringParameter()
{
Name = name,
DefaultValue = string.Empty,
IsRequired = false
};
}
///
/// Updates the values of the s in the
/// given parameter list using the values in the given connection string settings.
///
/// The list of parameters to be updated.
///
/// The connection string settings that is used to
/// determine whether the parameters should be bold or not.
///
private void UpdateConnectionStringParameters(List parameters, Dictionary settings)
{
if (m_suppressParameterValueUpdates || parameters is null)
return;
foreach (AdapterConnectionStringParameter parameter in parameters)
parameter.Value = settings.TryGetValue(parameter.Name, out string value) ? value : null;
}
private void OnAdapterTypeSelectedIndexChanged()
{
OnPropertyChanged("AdapterTypeSelectedIndex");
OnPropertyChanged("CustomConfigurationButtonVisibility");
}
private Assembly AppDomain_AssemblyResolve(object sender, ResolveEventArgs args)
{
try
{
AssemblyName assemblyName = new AssemblyName(args.Name);
string assemblyFile = Path.Combine(m_searchDirectory, $"{assemblyName.Name}.dll");
if (!File.Exists(assemblyFile))
assemblyFile = Path.Combine(m_searchDirectory, $"{assemblyName.Name}.exe");
if (!File.Exists(assemblyFile))
return null;
Assembly assembly = Assembly.LoadFrom(assemblyFile);
// JRC: Changed to allow more UI assembly load flexibility, like different versions
return !assembly.GetName().Name.Equals(assemblyName.Name) ? null : assembly;
}
catch
{
return null;
}
}
#endregion
}
}