//******************************************************************************************************
// PagedViewModelBase.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:
// ----------------------------------------------------------------------------------------------------
// 04/07/2011 - Mehulbhai P Thakkar
// Generated original version of source code.
// 05/13/2011 - Mehulbhai P Thakkar
// Modified CurrentItem.PropertyChanged event handler to virtual so that it can
// be overloaded by the derived classes for special handling such as by Measurements.cs
// 05/25/2011 - J. Ritchie Carroll
// Added load/save/delete event operations to allow for user control interception.
// 12/20/2012 - Starlynn Danyelle Gilliam
// Modified Header.
//
//******************************************************************************************************
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Threading;
using GSF.Data;
using GSF.TimeSeries.UI.Commands;
namespace GSF.TimeSeries.UI
{
///
/// Represents an abstract class with paging support which all ViewModel objects derive from.
///
public abstract class PagedViewModelBase : IViewModel where TDataModel : IDataModel, new()
{
#region [ Members ]
// Events
///
/// Raised when a property on this object has a new value.
///
public event PropertyChangedEventHandler PropertyChanged;
///
/// Raised before record load is executed.
///
public event CancelEventHandler BeforeLoad;
///
/// Raised when record has been loaded.
///
public event EventHandler Loaded;
///
/// Raised before record save is executed.
///
public event CancelEventHandler BeforeSave;
///
/// Raised when record has been saved.
///
public event EventHandler Saved;
///
/// Raised before record delete is executed.
///
public event CancelEventHandler BeforeDelete;
///
/// Raised when record has been deleted.
///
public event EventHandler Deleted;
// Fields
private int m_pageCount, m_currentPageNumber, m_itemsPerPage, m_currentSelectedIndex;
private IList m_itemsKeys;
private ObservableCollection m_currentPage, m_itemsSource;
private ObservableCollection> m_pages;
private ICommand m_firstCommand, m_previousCommand, m_nextCommand, m_lastCommand;
private RelayCommand m_saveCommand, m_deleteCommand, m_clearCommand;
private TDataModel m_currentItem;
private bool m_propertyChanged;
private bool m_autoSave;
private bool m_hideMessages;
private readonly DispatcherTimer m_timer;
private readonly TextBlock m_statusTextBlock;
private readonly TsfPopup m_statusPopup;
private Func m_sortSelector;
private string m_sortMember;
private string m_sortDirection;
#endregion
#region [ Constructors ]
///
/// Creates a new instance of the class.
///
/// Integer value to determine number of items per page.
/// Boolean value to determine is user changes should be saved automatically.
protected PagedViewModelBase(int itemsPerPage, bool autoSave = true)
{
m_itemsPerPage = itemsPerPage;
m_autoSave = autoSave;
Initialize();
if (itemsPerPage > 0)
Load();
m_timer = new DispatcherTimer();
m_timer.Interval = TimeSpan.FromSeconds(5);
m_timer.Tick += m_timer_Tick;
m_statusTextBlock = Application.Current.MainWindow?.FindName("TextBlockResult") as TextBlock;
m_statusPopup = Application.Current.MainWindow?.FindName("PopupStatus") as TsfPopup;
}
#endregion
#region [ Properties ]
///
/// Gets or sets current selected index on the UI data grid.
///
public int CurrentSelectedIndex
{
get
{
return m_currentSelectedIndex;
}
set
{
m_currentSelectedIndex = value;
OnPropertyChanged("CurrentSelectedIndex");
}
}
///
/// Gets or sets flag that determines if current item property has changed.
///
public bool CurrentItemPropertyChanged
{
get
{
return m_propertyChanged;
}
set
{
m_propertyChanged = value;
}
}
///
/// Gets or sets if user changes should be saved automatically.
///
public bool AutoSave
{
get
{
return m_autoSave;
}
set
{
m_autoSave = value;
}
}
///
/// Gets a message box to display message to users.
///
public Action Popup
{
get
{
return (Action)((message, caption, messageBoxImage) => MessageBox.Show(Application.Current.MainWindow, message, caption, MessageBoxButton.OK, messageBoxImage));
}
}
///
/// Gets message box to request confirmation from users.
///
public Func Confirm
{
get
{
return (Func)((message, caption) => MessageBox.Show(Application.Current.MainWindow, message, caption, MessageBoxButton.YesNo, MessageBoxImage.Question) == MessageBoxResult.Yes);
}
}
///
/// Gets or sets number of items to be displayed per page.
///
public int ItemsPerPage
{
get
{
return m_itemsPerPage;
}
set
{
m_itemsPerPage = value;
}
}
///
/// Gets or sets the collection of primary keys retrieved fromt he database.
///
public IList ItemsKeys
{
get
{
return m_itemsKeys;
}
set
{
m_itemsKeys = value;
}
}
///
/// Gets or sets the entire collection retrieved from the database.
///
public ObservableCollection ItemsSource
{
get
{
return m_itemsSource;
}
set
{
m_itemsSource = value;
OnPropertyChanged("ItemsSource");
GeneratePages();
}
}
///
/// Gets or sets the current item displayed in a form for edit.
///
public TDataModel CurrentItem
{
get
{
return m_currentItem;
}
set
{
if ((object)value != null)
{
if ((object)m_currentItem != null)
m_currentItem.PropertyChanged -= m_currentItem_PropertyChanged;
m_currentItem = value;
if ((object)m_currentItem != null)
m_currentItem.PropertyChanged += m_currentItem_PropertyChanged;
OnPropertyChanged("CurrentItem");
// If CurrentItem changes then raise OnPropertyChanged on CanDelete property.
// This will help us display or hide certain item based on IsNewItem property value.
OnPropertyChanged("CanDelete");
// If CurrentItem changes then raise OnPropertyChanged on IsNewRecord property.
// This will help us enable or disable Initialize button where applicable.
if ((object)m_currentItem != null)
OnPropertyChanged("IsNewRecord");
}
}
}
///
/// Gets or sets collection displayed on the current page.
///
public ObservableCollection CurrentPage
{
get
{
return m_currentPage;
}
set
{
if (m_currentPage != null)
m_currentPage.CollectionChanged -= m_currentPage_CollectionChanged;
m_currentPage = value;
OnPropertyChanged("CurrentPage");
if (m_currentPage != null)
{
m_currentPage.CollectionChanged += m_currentPage_CollectionChanged;
if (m_currentPage.Count > 0)
{
if (CurrentSelectedIndex < 0 || CurrentSelectedIndex >= m_currentPage.Count)
CurrentSelectedIndex = 0;
CurrentItem = m_currentPage[CurrentSelectedIndex];
}
}
}
}
///
/// Gets or sets an index of to be displayed.
///
public int CurrentPageNumber
{
get
{
return m_currentPageNumber;
}
set
{
if (m_currentPageNumber != value)
CurrentSelectedIndex = 0;
m_currentPageNumber = Math.Max(Math.Min(value, m_pageCount), 1);
OnPropertyChanged("CurrentPageNumber");
Load();
}
}
///
/// Gets or sets total number of pages.
///
public int PageCount
{
get
{
return m_pageCount;
}
set
{
m_pageCount = value;
OnPropertyChanged("PageCount");
}
}
///
/// Gets for moving to the first page.
///
public ICommand FirstCommand
{
get
{
if (m_firstCommand == null)
{
m_firstCommand = new RelayCommand
(
() =>
{
if (m_pages != null)
{
CurrentPage = m_pages[0];
SetCurrentPageNumber(1);
Load();
}
},
() =>
{
return (CurrentPageNumber - 1) < 1 ? false : true;
}
);
}
return m_firstCommand;
}
}
///
/// Gets for moving to the previous page.
///
public ICommand PreviousCommand
{
get
{
if (m_previousCommand == null)
{
m_previousCommand = new RelayCommand
(
() =>
{
if (m_pages != null)
{
SetCurrentPageNumber((CurrentPageNumber - 1) < 1 ? 1 : CurrentPageNumber - 1);
CurrentPage = m_pages[CurrentPageNumber - 1];
Load();
}
},
() =>
{
return (CurrentPageNumber - 1) < 1 ? false : true;
}
);
}
return m_previousCommand;
}
}
///
/// Gets for moving to the next page.
///
public ICommand NextCommand
{
get
{
if (m_nextCommand == null)
{
m_nextCommand = new RelayCommand
(
() =>
{
if (m_pages != null)
{
SetCurrentPageNumber((CurrentPageNumber + 1) > m_pageCount ? m_pageCount : CurrentPageNumber + 1);
CurrentPage = m_pages[CurrentPageNumber - 1];
Load();
}
},
() =>
{
return (CurrentPageNumber + 1) > m_pageCount ? false : true;
}
);
}
return m_nextCommand;
}
}
///
/// Gets for moving to the last page.
///
public ICommand LastCommand
{
get
{
if (m_lastCommand == null)
{
m_lastCommand = new RelayCommand
(
() =>
{
if (m_pages != null)
{
CurrentPage = m_pages[m_pageCount - 1];
SetCurrentPageNumber(m_pageCount);
Load();
}
},
() =>
{
return (CurrentPageNumber + 1) > m_pageCount ? false : true;
}
);
}
return m_lastCommand;
}
}
///
/// Gets save .
///
public ICommand SaveCommand
{
get
{
if (m_saveCommand == null)
m_saveCommand = new RelayCommand(Save, () => CanSave);
return m_saveCommand;
}
}
///
/// Gets delete .
///
public ICommand DeleteCommand
{
get
{
if (m_deleteCommand == null)
m_deleteCommand = new RelayCommand(Delete, () => CanDelete);
return m_deleteCommand;
}
}
///
/// Gets clear .
///
public ICommand ClearCommand
{
get
{
if (m_clearCommand == null)
m_clearCommand = new RelayCommand(Clear, () => CanClear);
return m_clearCommand;
}
}
///
/// Gets flag that indicates if state is valid and can be saved.
///
/// true if can be saved; otherwise false.
public virtual bool CanSave
{
get
{
if (CurrentItem == null)
return false;
else
return (CurrentItem.IsValid && (object)CommonFunctions.CurrentPrincipal != null && CommonFunctions.CurrentPrincipal.IsInRole("Administrator, Editor"));
}
}
///
/// Gets flag that indicates if can be deleted.
///
/// true if can be deleted; otherwise false.
public virtual bool CanDelete
{
get
{
if (CurrentItem == null)
return false;
else
return (!IsNewRecord && (object)CommonFunctions.CurrentPrincipal != null && CommonFunctions.CurrentPrincipal.IsInRole("Administrator, Editor"));
}
}
///
/// Gets flag that indicates if can be cleared.
///
/// true if can be cleared; otherwise false.
public virtual bool CanClear
{
get
{
return ((object)CommonFunctions.CurrentPrincipal == null || CommonFunctions.CurrentPrincipal.IsInRole("Administrator, Editor"));
}
}
///
/// Gets name of associated .
///
public virtual string DataModelName
{
get
{
return s_dataModelType.Name;
}
}
///
/// Gets flag that determines if is a new record.
///
public abstract bool IsNewRecord
{
get;
}
///
/// Gets or sets the function to transform the set of for sorting.
///
public Func SortSelector
{
get
{
return m_sortSelector;
}
set
{
m_sortSelector = value;
}
}
///
/// Gets or sets the member by which to sort the page.
///
public string SortMember
{
get
{
return m_sortMember;
}
set
{
m_sortMember = value;
}
}
///
/// Gets or sets the direction in which to sort the page.
///
public string SortDirection
{
get
{
return m_sortDirection;
}
set
{
m_sortDirection = value;
}
}
#endregion
#region [ Methods ]
///
/// Handles timer's tick event to display status messages.
///
///
///
protected virtual void m_timer_Tick(object sender, EventArgs e)
{
if (m_statusPopup != null)
{
if (!m_statusPopup.IsOpen)
{
m_statusPopup.IsOpen = true;
}
else
{
m_statusPopup.IsOpen = false;
m_timer.Stop();
}
}
}
///
/// Gets the primary key value of the .
///
/// The primary key value of the .
public abstract TPrimaryKey GetCurrentItemKey();
///
/// Gets the string based named identifier of the .
///
/// The string based named identifier of the .
public abstract string GetCurrentItemName();
///
/// Displays status messages as a auto closing popup.
///
/// Text to be displayed on popup window.
/// Boolean flag indicating success.
public virtual bool DisplayStatusMessage(string statusText)
{
if (m_statusTextBlock != null && m_statusPopup != null)
{
m_statusTextBlock.Text = statusText;
m_statusPopup.IsOpen = true;
m_timer.Start();
return true;
}
return false;
}
///
/// Initialization to be done before the initial call to .
///
public virtual void Initialize()
{
PageCount = 0;
CurrentPage = new ObservableCollection();
CurrentItem = new TDataModel();
SetCurrentPageNumber(0);
}
///
/// Loads the records for the associated .
///
public virtual void Load()
{
Mouse.OverrideCursor = Cursors.Wait;
List pageKeys = null;
try
{
if (OnBeforeLoadCanceled())
throw new OperationCanceledException("Load was canceled.");
// Load keys if LoadKeys method exists in data model
if ((object)m_itemsKeys == null && (object)s_loadKeys != null)
m_itemsKeys = (IList)s_loadKeys.Invoke(this, new object[] { (AdoDataConnection)null, SortMember, SortDirection });
// Sort keys if a sort selector has been specified
if ((object)m_itemsKeys != null && (object)SortSelector != null)
{
if (SortDirection == "ASC")
m_itemsKeys = m_itemsKeys.OrderBy(SortSelector).ToList();
else
m_itemsKeys = m_itemsKeys.OrderByDescending(SortSelector).ToList();
}
// Extract a single page of keys
if ((object)m_itemsKeys != null)
pageKeys = m_itemsKeys.Skip((CurrentPageNumber - 1) * ItemsPerPage).Take(ItemsPerPage).ToList();
// If we were able to extract a page of keys, load only that page.
// Otherwise, load the whole recordset.
if ((object)pageKeys != null)
ItemsSource = (ObservableCollection)s_loadRecords.Invoke(this, new object[] { (AdoDataConnection)null, pageKeys });
else
ItemsSource = (ObservableCollection)s_loadRecords.Invoke(this, new object[] { (AdoDataConnection)null });
OnLoaded();
}
catch (Exception ex)
{
if (ex.InnerException != null)
{
Popup(ex.Message + Environment.NewLine + "Inner Exception: " + ex.InnerException.Message, "Load " + DataModelName + " Exception:", MessageBoxImage.Error);
CommonFunctions.LogException(null, "Load " + DataModelName, ex.InnerException);
}
else
{
Popup(ex.Message, "Load " + DataModelName + " Exception:", MessageBoxImage.Error);
CommonFunctions.LogException(null, "Load " + DataModelName, ex);
}
}
finally
{
Mouse.OverrideCursor = null;
}
}
///
/// Saves the record for the associated .
///
public virtual void Save()
{
if (CanSave)
{
try
{
if (OnBeforeSaveCanceled())
throw new OperationCanceledException("Save was canceled.");
bool isNewRecord = IsNewRecord;
string result = (string)s_saveRecord.Invoke(this, new object[] { (AdoDataConnection)null, CurrentItem });
OnSaved();
m_propertyChanged = false; // after saving information, set this flag to false.
if (!m_hideMessages)
{
if (m_statusTextBlock != null)
{
DisplayStatusMessage(result);
}
else
{
Popup(result, "Save " + DataModelName, MessageBoxImage.Information);
}
}
if (isNewRecord)
{
ItemsKeys = null;
Load();
}
}
catch (Exception ex)
{
if (ex.InnerException != null)
{
Popup(ex.Message + Environment.NewLine + "Inner Exception: " + ex.InnerException.Message, "Save " + DataModelName + " Exception:", MessageBoxImage.Error);
CommonFunctions.LogException(null, "Save " + DataModelName, ex.InnerException);
}
else
{
Popup(ex.Message, "Save " + DataModelName + " Exception:", MessageBoxImage.Error);
CommonFunctions.LogException(null, "Save " + DataModelName, ex);
}
}
}
}
///
/// Deletes the record for the associated .
///
public virtual void Delete()
{
if (CanDelete && Confirm("Are you sure you want to delete \'" + GetCurrentItemName() + "\'?", "Delete " + DataModelName))
{
try
{
if (OnBeforeDeleteCanceled())
throw new OperationCanceledException("Delete was canceled.");
TPrimaryKey currentItemKey = GetCurrentItemKey();
string result = (string)s_deleteRecord.Invoke(this, new object[] { (AdoDataConnection)null, currentItemKey });
if ((object)m_itemsKeys != null)
m_itemsKeys.Remove(currentItemKey);
OnDeleted();
Load();
if (m_statusTextBlock != null)
{
DisplayStatusMessage(result);
}
else
{
Popup(result, "Delete " + DataModelName, MessageBoxImage.Information);
}
}
catch (Exception ex)
{
if (ex.InnerException != null)
{
Popup(ex.Message + Environment.NewLine + "Inner Exception: " + ex.InnerException.Message, "Delete " + DataModelName + " Exception:", MessageBoxImage.Error);
CommonFunctions.LogException(null, "Delete " + DataModelName, ex.InnerException);
}
else
{
Popup(ex.Message, "Delete " + DataModelName + " Exception:", MessageBoxImage.Error);
CommonFunctions.LogException(null, "Delete " + DataModelName, ex);
}
}
}
}
///
/// Clears the record for the associated .
///
public virtual void Clear()
{
if (CanClear)
{
CurrentItem = new TDataModel();
}
}
///
/// Method to check if property has changed and if so then either save or ask user's confirmation.
///
public virtual void ProcessPropertyChange()
{
if (m_propertyChanged)
{
int currentSelectedIndex = CurrentSelectedIndex;
m_propertyChanged = false;
if (CanSave)
{
if (AutoSave)
{
m_hideMessages = true;
Save();
m_hideMessages = false;
}
else if (Confirm("\'" + GetCurrentItemName() + "\' has changed. Do you want to save changes?", "Save " + DataModelName))
{
Save();
}
else
{
Load();
Dispatcher.CurrentDispatcher.BeginInvoke(new Action(() => CurrentSelectedIndex = currentSelectedIndex));
}
}
else
{
Load();
Dispatcher.CurrentDispatcher.BeginInvoke(new Action(() => CurrentSelectedIndex = currentSelectedIndex));
}
}
}
///
/// Sets the current page number without initiating a call to .
///
/// The new value for the current page number.
protected void SetCurrentPageNumber(int currentPageNumber)
{
if (m_currentPageNumber != currentPageNumber)
CurrentSelectedIndex = 0;
m_currentPageNumber = currentPageNumber;
OnPropertyChanged("CurrentPageNumber");
}
///
/// Raises the event.
///
/// Property name that has changed.
protected virtual void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
///
/// Raises the event.
///
protected virtual bool OnBeforeLoadCanceled()
{
CancelEventArgs cancelEventArgs = new CancelEventArgs();
if (BeforeLoad != null)
BeforeLoad(this, cancelEventArgs);
return cancelEventArgs.Cancel;
}
///
/// Raises the event.
///
protected virtual void OnLoaded()
{
if (Loaded != null)
Loaded(this, EventArgs.Empty);
}
///
/// Raises the event.
///
protected virtual bool OnBeforeSaveCanceled()
{
CancelEventArgs cancelEventArgs = new CancelEventArgs();
if (BeforeSave != null)
BeforeSave(this, cancelEventArgs);
return cancelEventArgs.Cancel;
}
///
/// Raises the event.
///
protected virtual void OnSaved()
{
if (Saved != null)
Saved(this, EventArgs.Empty);
}
///
/// Raises the event.
///
protected virtual bool OnBeforeDeleteCanceled()
{
CancelEventArgs cancelEventArgs = new CancelEventArgs();
if (BeforeDelete != null)
BeforeDelete(this, cancelEventArgs);
return cancelEventArgs.Cancel;
}
///
/// Raises the event.
///
protected virtual void OnDeleted()
{
if (Deleted != null)
Deleted(this, EventArgs.Empty);
}
// We monitor for changes to IsValid property on current item so that we can propagate
// this change notification to CanSave
///
/// Handles PropertyChanged event on CurrentItem.
///
/// Source of the event.
/// Event arguments.
protected virtual void m_currentItem_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
// Change Save button appearance based on IsValid property value.
// If value of IsValid is changed then raise OnPropertyChanged for CanSave to enable or disable Save button.
if (string.Compare(e.PropertyName, "IsValid", true) == 0)
OnPropertyChanged("CanSave");
m_propertyChanged = true;
}
// We monitor the changes to the collection currently displayed on the screen.
// If user tried to delete then process it.
private void m_currentPage_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Remove)
{
foreach (object item in e.OldItems)
{
CurrentItem = (TDataModel)item;
Delete();
//s_deleteRecord.Invoke(this, new object[] { (AdoDataConnection)null, GetCurrentItemKey() });
}
}
}
///
/// Method to create a list of pages based on the .
///
protected void GeneratePages()
{
if (ItemsSource != null && ItemsSource.Count > 0)
{
if (ItemsPerPage > 0)
{
if ((object)ItemsKeys != null)
{
PageCount = (int)Math.Ceiling(ItemsKeys.Count / (double)ItemsPerPage);
if ((object)m_pages == null)
m_pages = new ObservableCollection>();
while (m_pages.Count < m_pageCount)
m_pages.Add(new ObservableCollection());
SetCurrentPageNumber(Math.Max(Math.Min(CurrentPageNumber, m_pageCount), 1));
m_pages[CurrentPageNumber - 1] = ItemsSource;
CurrentPage = m_pages[CurrentPageNumber - 1];
}
else
{
m_pages = new ObservableCollection>();
PageCount = (int)Math.Ceiling(ItemsSource.Count / (double)ItemsPerPage);
for (int i = 0; i < m_pageCount; i++)
{
ObservableCollection page = new ObservableCollection();
for (int j = 0; j < ItemsPerPage; j++)
{
if (i * ItemsPerPage + j > ItemsSource.Count - 1)
break;
page.Add(ItemsSource[i * ItemsPerPage + j]);
}
m_pages.Add(page);
}
if (CurrentPage == null || CurrentPageNumber == 0)
{
CurrentPage = m_pages[0];
SetCurrentPageNumber(1);
}
else
{
// Retain current page when user deletes any record from the collection
SetCurrentPageNumber((CurrentPageNumber + 1) > m_pageCount ? m_pageCount : CurrentPageNumber);
CurrentPage = m_pages[CurrentPageNumber - 1];
}
}
}
}
else
{
PageCount = 0;
CurrentPage = new ObservableCollection();
SetCurrentPageNumber(0);
Clear();
}
}
///
/// Sorts model data.
///
/// Member path for sorting.
public virtual void SortData(string sortMemberPath)
{
ItemsSource = new ObservableCollection(
from item in ItemsSource
orderby typeof(TDataModel).GetProperty(sortMemberPath).GetValue(item, null)
select item
);
}
///
/// Sorts model data.
///
/// Member path for sorting.
/// Ascending or descending.
public virtual void SortData(string sortMemberPath, ListSortDirection sortDirection)
{
SortSelector = null;
SortMember = sortMemberPath;
SortDirection = (sortDirection == ListSortDirection.Descending) ? "DESC" : "ASC";
ItemsKeys = null;
Load();
}
///
/// Sorts model data.
///
/// Transform function for sorting.
/// Ascending or descending.
public virtual void SortDataBy(Func sortSelector, ListSortDirection sortDirection)
{
SortSelector = sortSelector;
SortMember = string.Empty;
SortDirection = (sortDirection == ListSortDirection.Descending) ? "DESC" : "ASC";
Load();
}
#endregion
#region [ Static ]
// Static Fields
private static Type s_dataModelType = typeof(TDataModel);
private static MethodInfo s_loadRecords = s_dataModelType.GetMethod("Load");
private static MethodInfo s_saveRecord = s_dataModelType.GetMethod("Save");
private static MethodInfo s_deleteRecord = s_dataModelType.GetMethod("Delete");
private static MethodInfo s_loadKeys = s_dataModelType.GetMethod("LoadKeys");
#endregion
}
}