namespace Framework.Json
{
using Database.dbo;
using Framework.App;
using Framework.DataAccessLayer;
using Framework.Json.Bulma;
using Framework.Server;
using Framework.Session;
using Microsoft.AspNetCore.Http;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Dynamic.Core;
using System.Text.Json;
using System.Threading.Tasks;
///
/// Request command sent by browser or internally by server.
/// See also enum: GridIsClickEnum.
///
internal enum CommandEnum
{
None = 0,
ButtonIsClick = 1,
GridIsClickSort = 8,
GridCellIsModify = 9,
///
/// See also enum GridIsClickEnum for details of this command.
///
GridIsClickEnum = 10,
GridIsClickRow = 11,
///
/// User clicked data grid header column config button.
///
GridIsClickConfig = 12,
///
/// Inform server about text leave event.
///
GridIsTextLeave = 13,
///
/// User clicked home button for example on navbar.
///
HomeIsClick = 15,
///
/// User clicked an internal link. For example: "/contact/". Instead of GET and download Angular again a POST command is sent to the server.
///
NavigatePost = 16,
///
/// User clicked backward or forward button in browser.
///
NavigateBackwardForward = 17,
BootstrapNavbarButtonIsClick = 7,
///
/// User clicked button on bulma navbar.
///
BulmaNavbarItemIsClick = 18,
///
/// User clicked button on bulma navbar.
///
BulmaNavbarMenuItemIsClick = 22,
///
/// User clicked button on Html json component.
///
HtmlButtonIsClick = 19,
///
/// User resized column width.
///
StyleColumnWidth = 20,
///
/// User clicked number on dialpad.
///
Dialpad = 21,
// Next = 23,
}
///
/// Origin for request and command.
///
internal enum RequestOrigin
{
None = 0,
///
/// Request or command created by server.
///
Server = 1,
///
/// Request or command sent by browser.
///
Browser = 2,
}
///
/// Command sent by browser. See also class RequestJson.
///
internal sealed class CommandJson
{
public CommandEnum CommandEnum { get; set; }
public int GridCellId { get; set; }
public int RowStateId { get; set; }
public string GridCellText { get; set; }
///
/// Gets or sets GridCellTextBase64. Contains from user uploaded file data.
///
public string GridCellTextBase64 { get; set; }
///
/// Gets or sets GridCellTextBase64. Contains file name from user uploaded file.
///
public string GridCellTextBase64FileName { get; set; }
///
/// Gets or sets Id. This is ComponentJson.Id.
///
public int ComponentId { get; set; }
public GridIsClickEnum GridIsClickEnum { get; set; }
///
/// Gets GridCellTextIsInternal. If true, text has been set internally by grid lookup select row.
///
public bool GridCellTextIsLookup; // TODO Command Queue
public int BootstrapNavbarButtonId { get; set; }
public int BulmaNavbarItemId { get; set; }
public string BulmaFilterText { get; set; }
///
/// Gets or sets NavigatePath. For internal link. For example: "/contact/".
///
public string NavigatePath { get; set; }
///
/// Gets or sets NavigatePathIsAddHistory. If true, NavigatePath is added to browser history. Used by server command. Not used by client command.
///
public bool NavigatePathIsAddHistory { get; set; }
///
/// Gets or sets HtmlButtonId. If user clicked button in Html json component this is its id.
///
public string HtmlButtonId { get; set; }
///
/// Gets or sets ResizeColumnIndex. User resized IsVisibleScroll column with this index.
///
public int ResizeColumnIndex { get; set; }
///
/// Gets or sets ResizeColumnWidthValue. This is the new column width.
///
public double ResizeColumnWidthValue { get; set; }
///
/// Gets or sets DialpadText. This is the number sent by dialpad.
///
public string DialpadText { get; set; }
}
///
/// Request sent by Angular client.
///
internal sealed class RequestJson
{
///
/// Constructor.
///
public RequestJson() { }
///
/// Constructor.
///
public RequestJson(CommandJson command)
{
this.Origin = RequestOrigin.Server;
this.CommandList = new List();
if (command != null)
{
this.CommandList.Add(command);
}
}
public int RequestCount { get; set; }
public int ResponseCount { get; set; }
///
/// Gets or sets Origin. Request sent by browser or created by server.
///
public RequestOrigin Origin { get; set; }
///
/// Gets or sets BrowserNavigatePathPost. Url shown in browser. Available only for browser POST request. See also method BrowserPath();
///
public string BrowserNavigatePathPost { get; set; }
///
/// Gets BrowserNavigatePath. For example "/contact/".
///
public string BrowserNavigatePath
{
get
{
string result;
if (Origin == RequestOrigin.Server)
{
UtilFramework.Assert(UtilServer.Context.Request.Method == "GET");
result = UtilServer.Context.Request.Path; // Browser refresh
}
else
{
UtilFramework.Assert(Origin == RequestOrigin.Browser);
UtilFramework.Assert(UtilServer.Context.Request.Method == "POST");
UtilFramework.Assert(BrowserNavigatePathPost != null);
result = new Uri(BrowserNavigatePathPost).AbsolutePath; // Browser back
}
return result;
}
}
///
/// Gets or sets CommandList. Command queue sent by one browser request. Commands can also be added during process.
///
public List CommandList { get; set; }
///
/// Gets or sets CommandListIndex. This is the current command to process.
///
[Serialize(SerializeEnum.None)]
public int CommandIndex { get; set; }
///
/// Returns current command to process.
///
public CommandJson CommandGet()
{
CommandJson result = null;
if (CommandList.Count > CommandIndex)
{
result = CommandList[CommandIndex];
}
return result;
}
[Serialize(SerializeEnum.None)]
public int CommandAddCount;
///
/// Add command on top to queue.
///
public void CommandAdd(CommandJson command)
{
CommandAddCount += 1;
if (CommandAddCount == 8)
{
throw new Exception("CommandAdd overflow!");
}
CommandList.Add(command);
}
///
/// Move to next command in queue.
///
public void CommandNext()
{
if (CommandList.Count > CommandIndex)
{
CommandIndex += 1;
}
}
}
///
/// Used by method UtilJson.Serialize(); to determine whether ComponentJson or Dto should be sent to client if stored in a list.
///
public interface IHide
{
///
/// Gets IsHide. If true, ComponentJson or Dto is not sent to client if stored in a list.
///
public bool IsHide { get; }
}
///
/// Application component tree. Tree is serialized and deserialized for every client request. Stores session state from public and internal fields and properties.
///
public abstract class ComponentJson : IHide
{
///
/// Constructor to programmatically create new object. Constructor is not called on client request session deserialization (GetUninitializedObject).
///
internal ComponentJson(ComponentJson owner, string type)
{
this.Type = type;
Constructor(owner, isDeserialize: false);
}
internal void Constructor(ComponentJson owner, bool isDeserialize)
{
this.Owner = owner;
if (Owner == null)
{
this.Root = this;
this.RootComponentJsonList = new Dictionary(); // Init list.
this.RootReferenceList = new List<(object obj, UtilJson.DeclarationProperty property, int id)>();
}
else
{
this.Root = owner.Root;
}
if (!isDeserialize)
{
Root.RootIdCount += 1;
this.Id = Root.RootIdCount;
Root.RootComponentJsonList.Add(Id, this); // Id is not yet available if deserialize.
}
if (isDeserialize == false)
{
if (owner != null)
{
if (owner.List == null)
{
owner.List = new List();
}
int count = 0;
foreach (var item in owner.List)
{
if (item.TrackBy.StartsWith(this.Type + "-"))
{
count += 1;
}
}
this.TrackBy = this.Type + "-" + count.ToString();
owner.ListInternal.Add(this);
}
}
}
///
/// Gets Owner. This is the parent of this component.
///
[Serialize(SerializeEnum.None)]
public ComponentJson Owner { get; internal set; }
[Serialize(SerializeEnum.None)]
internal bool IsRemoved;
[Serialize(SerializeEnum.None)]
internal ComponentJson Root;
internal int RootIdCount;
///
/// (Id, ComponentJson).
///
[Serialize(SerializeEnum.None)]
internal Dictionary RootComponentJsonList;
///
/// (Object, Property, ReferenceId). Used for deserialization.
///
[Serialize(SerializeEnum.None)]
internal List<(object obj, UtilJson.DeclarationProperty property, int id)> RootReferenceList;
///
/// Solve ComponentJson references after deserialization.
///
internal void RootReferenceSolve()
{
UtilFramework.Assert(Owner == null);
UtilFramework.Assert(Root == this);
foreach (var item in Root.RootReferenceList)
{
UtilFramework.Assert(item.property.IsList == false, "Reference to ComponentJson in List not supported!");
ComponentJson componentJson = Root.RootComponentJsonList[item.id]; // Exception: Given key was not present in dictionary. Do not use method ComponentJson.ListInternal.Remove(); use method ComponentJsonExtension.ComponentRemove();
item.property.ValueSet(item.obj, componentJson);
}
}
///
/// Gets Id. Client sends command to server. See also field
///
internal int Id { get; set; }
///
/// Gets or sets Type. Used by Angular. Type to be rendered also for derived classes. See also class .
/// Used by Angular client. Not used by server for serialization or deserialization.
///
internal string Type;
internal string TrackBy { get; set; }
///
/// Gets or sets custom html style classes for this component.
///
public string CssClass;
[Serialize(SerializeEnum.None)]
internal List ListInternal = new List(); // Empty list is removed by json serializer.
///
/// Gets List. List of child components.
///
public IReadOnlyList List
{
get
{
return ListInternal;
}
internal set
{
ListInternal = (List)value;
}
}
///
/// Gets or sets IsHide. If true component is not sent to client.
///
public bool IsHide { get; set; }
}
///
/// Extension methods to manage json component tree.
///
public static class ComponentJsonExtension
{
///
/// Returns owner of type T. Searches in parent and grand parents.
///
public static T ComponentOwner(this ComponentJson component) where T : ComponentJson
{
do
{
component = component.Owner;
if (component is T)
{
return (T)component;
}
} while (component != null);
return null;
}
private static void ComponentListAll(ComponentJson component, List result)
{
result.AddRange(component.List);
foreach (var item in component.List)
{
ComponentListAll(item, result);
}
}
///
/// Returns list of all child components recursive including this.
///
public static List ComponentListAll(this ComponentJson component)
{
List result = new List
{
component
};
ComponentListAll(component, result);
return result;
}
///
/// Returns list of all child components recursive including this of type T.
///
public static List ComponentListAll(this ComponentJson component) where T : ComponentJson
{
return component.ComponentListAll().OfType().ToList();
}
///
/// Returns all child components of type T.
///
public static List ComponentList(this ComponentJson component) where T : ComponentJson
{
return component.List.OfType().ToList();
}
public enum PageShowEnum
{
None = 0,
///
/// Add page and remove sibling pages.
///
SiblingRemove = 1,
///
/// Add page and hide sibling pages and keep their state.
///
SiblingHide = 2,
}
///
/// Shows page or creates new one if it does not yet exist. Invokes also page init async.
///
public static async Task ComponentPageShowAsync(this ComponentJson owner, T page, PageShowEnum pageShowEnum = PageShowEnum.SiblingRemove, Action init = null) where T : Page
{
T result = page;
if (page != null && page.IsRemoved == false)
{
UtilFramework.Assert(page.Owner == owner, "Wrong Page.Owner!");
}
if (pageShowEnum == PageShowEnum.SiblingHide)
{
foreach (Page item in owner.List.OfType())
{
item.IsHide = true; // Hide
}
}
if (page == null || page.IsRemoved)
{
result = (T)Activator.CreateInstance(typeof(T), owner);
init?.Invoke(result);
await result.InitAsync();
}
result.IsHide = false; // Show
if (pageShowEnum == PageShowEnum.SiblingRemove)
{
owner.List.OfType().ToList().ForEach(page =>
{
if (page != result) { page.ComponentRemove(); }
});
}
return result;
}
///
/// Creates new page. Invokes also page init async.
///
public static Task ComponentPageShowAsync(this ComponentJson owner, PageShowEnum pageShowEnum = PageShowEnum.None, Action init = null) where T : Page
{
return ComponentPageShowAsync(owner, null, pageShowEnum, init);
}
///
/// Remove this component.
///
public static void ComponentRemove(this ComponentJson component)
{
if (component != null)
{
foreach (var item in component.ComponentListAll())
{
item.Owner?.ListInternal.Remove(item);
item.Owner = null;
item.IsRemoved = true;
}
}
}
///
/// Returns index of this component in parents list.
///
public static int ComponentIndex(this ComponentJson component)
{
return component.Owner.ListInternal.IndexOf(component);
}
///
/// Returns count of this component parents list.
///
public static int ComponentCount(this ComponentJson component)
{
return component.Owner.List.Count();
}
///
/// Move this component to index position.
///
public static void ComponentMove(this ComponentJson component, int index)
{
var list = component?.Owner.ListInternal;
list.Remove(component);
list.Insert(index, component);
}
///
/// Move this component to last index.
///
public static void ComponentMoveLast(this ComponentJson component)
{
component.ComponentMove(component.ComponentCount() - 1);
}
///
/// Remove all children.
///
public static void ComponentListClear(this ComponentJson component)
{
foreach (var item in component.ListInternal)
{
item.Owner = null;
item.IsRemoved = true;
}
component.ListInternal.Clear();
}
///
/// Add css class to ComponentJson.
///
public static void CssClassAdd(this ComponentJson component, string value)
{
string cssClass = component.CssClass;
string cssClassWholeWord = " " + cssClass + " ";
if (!cssClassWholeWord.Contains(" " + value + " "))
{
if (UtilFramework.StringNull(cssClass) == null)
{
component.CssClass = value;
}
else
{
component.CssClass += " " + value;
}
}
}
///
/// Remove css class from ComponentJson.
///
public static void CssClassRemove(this ComponentJson component, string value)
{
string cssClass = component.CssClass;
string cssClassWholeWord = " " + cssClass + " ";
if (cssClassWholeWord.Contains(" " + value + " "))
{
component.CssClass = cssClassWholeWord.Replace(" " + value + " ", "").Trim();
}
}
}
///
/// Css framework to render for example generic class Alert.
///
public enum CssFrameworkEnum
{
///
/// No css framework is used. For example class Alert is not available.
/// See also: Framework/Framework.Cli/Template/Application.Website/
/// See also: Application.Website/ (if applicable)
///
None = 0,
///
/// Bootstrap css framework is used.
/// See also: https://getbootstrap.com/
/// See also: Framework/Framework.Cli/Template/Application.Website/
/// See also: Application.Website/ (if applicable)
///
Bootstrap = 1,
///
/// Bulma css framework is used.
/// See also: https://bulma.io/
/// See also: Framework/Framework.Cli/Template/Application.Website/
/// See also: Application.Website/ (if applicable)
///
Bulma = 2
}
public class AppJson : Page
{
public AppJson()
: base(null)
{
}
///
/// Gets or sets Title. This is the html title.
///
public string Title { get; set; }
///
/// Gets or sets Description. This is the html meta description.
///
public string Description { get; set; }
///
/// Gets or sets CssFrameworkEnum. Switch between Bootstrap and Bulma framework.
///
public CssFrameworkEnum CssFrameworkEnum { get; set; }
///
/// Gets or sets AlertContainer. Can be used for class Alert to display messages at the right place. For example if there is a stick top navigation bar.
///
public ComponentJson AlertContainer { get; set; }
///
/// Returns AlertContainer or AppJson.
///
public ComponentJson AlertContainerGet()
{
return AlertContainer != null ? AlertContainer : this;
}
///
/// Returns settings for currently logged in user. Used for example by class Grid to determine if developer is logged in to configure data grid.
///
protected virtual void Setting(SettingArgs args, SettingResult result)
{
}
public class SettingArgs
{
///
/// Gets Grid. Settings for this grid are requested.
///
public Grid Grid { get; internal set; }
}
public class SettingResult
{
///
/// Gets or sets GridIsShowConfig. If true, data grid config button is shown.
///
public bool GridIsShowConfig { get; set; }
///
/// Gets or sets GridIsShowConfigDeveloper. If true, grid shows config developer (coffee icon) button.
///
public bool GridIsShowConfigDeveloper { get; set; }
}
internal static SettingResult SettingInternal(ComponentJson component, SettingArgs args)
{
var result = new SettingResult();
component.ComponentOwner()?.Setting(args, result);
return result;
}
///
/// Returns NamingConvention for app related sql tables.
///
internal virtual NamingConvention NamingConventionApp()
{
return new NamingConvention();
}
internal async Task NavigateInternalAsync(string navigatePath, AppSelector appSelector)
{
var args = new NavigateArgs(navigatePath, UtilServer.Context.Request.Query, UtilServer.RequestUrlHost(), appSelector.ConfigDomain.Custom);
NavigateResult result = new NavigateResult();
await NavigateAsync(args, result);
UtilFramework.Assert(!(result.Data != null && result.IsSession), $"Method {nameof(AppJson.NavigateAsync)}(); can not send data and request server session deserialization at the same time!");
return result;
}
///
/// Browser requests (GET) file to download or navigate to subpage. Inside this method no session data is available. Used for example to download a public available (*.pdf) file.
/// If no data or IsSession flag is returned HTTP 404 code page not found is sent to browser. Method is not called for root NavigatePath "/" or if application is running embedded.
/// It is also possible to request IsSession and then return in the method NavigateSessionAsync(); a custom 404 page not found.
///
protected internal virtual Task NavigateAsync(NavigateArgs args, NavigateResult result)
{
return Task.FromResult(0);
}
public class NavigateArgs
{
internal NavigateArgs(string navigatePath, IQueryCollection httpQuery, string requestUrlHost, object configCustom)
{
NavigatePath = navigatePath;
HttpQuery = httpQuery;
RequestUrlHost = requestUrlHost;
ConfigCustom = configCustom;
if (UtilServer.NavigatePathIsFileName(navigatePath))
{
FileName = UtilFramework.FolderNameParse(null, navigatePath);
FileNameExtension = UtilFramework.FileNameExtension(FileName);
}
}
///
/// Gets NavigatePath. For example: "/Readme.txt" or "/contact/". Or "/", if user clicked browser back button.
///
public string NavigatePath { get; private set; }
///
/// Gets FileName. For example: "Readme.txt" or "/doc/Readme.txt". Is null, if navigatePath is for example: "/contact/". See also method IsFileName(); to extract it.
///
public string FileName { get; private set; }
///
/// Gets FileNameExtension. For example: ".txt".
///
public string FileNameExtension { get; private set; }
///
/// Gets HttpQuery. Determine for example: "/doc/image.png?thumbnail"
///
public IQueryCollection HttpQuery { get; private set; }
///
/// Gets RequestHostUrl. For example: "http://localhost:5000/".
///
public string RequestUrlHost { get; }
///
/// Gets ConfigCustom. This is domain custom configuration data from file ConfigServer.json.
///
public object ConfigCustom { get; }
///
/// Returns true, if navigatePath starts with navigatePathPrefix.
///
/// For example: "/doc/".
/// For example: "/contact/".
public bool IsNavigatePath(string navigatePathPrefix, out string navigatePath)
{
if (!navigatePathPrefix.StartsWith("/"))
{
navigatePathPrefix = "/" + navigatePathPrefix;
}
if (!navigatePathPrefix.EndsWith("/"))
{
navigatePathPrefix += "/";
}
bool result = NavigatePath.StartsWith(navigatePathPrefix);
if (result)
{
navigatePath = NavigatePath.Substring(navigatePathPrefix.Length - 1);
}
else
{
navigatePath = null;
}
return result;
}
///
/// Returns true, if navigatePath starts with navigatePathPrefix.
///
/// For example: "/doc/".
public bool IsNavigatePath(string navigatePathPrefix)
{
return IsNavigatePath(navigatePathPrefix, out _);
}
///
/// Returns true, if navigatePath starts with navigatePathPrefix.
///
/// For example: "/doc/file/".
/// For example: "about/Logo.png".
public bool IsFileName(string navigatePathPrefix, out string fileName)
{
bool result = false;
fileName = null;
if (FileName != null)
{
if (!navigatePathPrefix.StartsWith("/"))
{
navigatePathPrefix = "/" + navigatePathPrefix;
}
if (!navigatePathPrefix.EndsWith("/"))
{
navigatePathPrefix += "/";
}
result = NavigatePath.StartsWith(navigatePathPrefix);
if (result)
{
fileName = FileName.Substring(navigatePathPrefix.Length - "/".Length);
}
}
return result;
}
}
public class NavigateResult
{
///
/// Gets or sets IsSession. If true, session data is deserialized and method NavigateSessionAsync(); is called next.
/// If false, a (HTTP 404) page not found is sent to the client.
///
public bool IsSession { get; set; }
///
/// Gets or sets Data. If not null, this is the file data sent to the browser to download.
///
public byte[] Data;
///
/// Gets or sets RedirectPath. Used to redirect trailing slash. For example redirect "/contact" to "/contact/".
/// Sends a redirect response (HTTP 302) to the client.
///
public string RedirectPath { get; set; }
}
internal async Task NavigateSessionInternalAsync(string navigatePath, bool isAddHistory, AppSelector appSelector)
{
var args = new NavigateArgs(navigatePath, UtilServer.Context.Request.Query, UtilServer.RequestUrlHost(), (JsonElement?)appSelector.ConfigDomain.Custom);
var result = new NavigateSessionResult { NavigatePath = args.NavigatePath };
await NavigateSessionAsync(args, result);
if (result.IsPage)
{
if (UtilServer.Context.Request.Method == "GET")
{
// Do not add history entry for any GET
isAddHistory = false;
}
if (RequestJson.CommandList.FirstOrDefault()?.CommandEnum == CommandEnum.NavigateBackwardForward)
{
// Do not add history entry if user clicked backward or forward button in browser.
isAddHistory = false;
}
if (isAddHistory)
{
if (NavigatePathAddHistory != null)
{
// Allow multiple calls of method Navigate();
// throw new Exception(string.Format("Only one PathAddHistory entry possible for one request! ({0}, {1})", NavigatePathAddHistory, result.NavigatePath));
}
NavigatePathAddHistory = result.NavigatePath;
}
}
else
{
Download(result.Data, args.FileName);
}
return result;
}
///
/// Browser requests file to download or navigate to subpage. Inside this method session data is available. Used for example to download a NOT publicly available (*.pdf) file.
/// Also called when user clicked backward or forward button in browser or if application is running embedded. It is also possible to return a custom 404 page note found.
///
protected internal virtual Task NavigateSessionAsync(NavigateArgs args, NavigateSessionResult result)
{
return Task.FromResult(0);
}
public class NavigateSessionResult
{
///
/// Gets IsPage. If true, requested url is a page. If false (and Data not null) requested url is a file.
///
public bool IsPage
{
get
{
return Data == null;
}
}
///
/// Gets or sets Data. If not null, this is the file data sent to the browser to download.
///
public byte[] Data;
///
/// Gets or sets IsPageNotFound. If true, page is sent together with HTTP status code 404.
///
public bool IsPageNotFound { get; set; }
///
/// Gets or sets NavigatePath. For example: "/contact/" or "/signin/", if redirected. Used for example in single page application (SPA) if user clicked a button to navigate to a page.
///
public string NavigatePath { get; set; }
///
/// Gets or sets RedirectPath. Used to redirect trailing slash. For example redirect "/contact" to "/contact/".
/// Sends a redirect response (HTTP 302) to the client.
///
public string RedirectPath { get; set; }
}
///
/// Add navigate command to queue.
///
/// For example "/contact/"
/// If true, navigatePath is added to browser history.
internal void Navigate(string navigatePath, bool isAddHistory)
{
this.RequestJson.CommandAdd(new CommandJson {
CommandEnum = CommandEnum.NavigatePost,
ComponentId = Id,
NavigatePath = navigatePath,
NavigatePathIsAddHistory = isAddHistory });
}
///
/// Add navigate command to queue.
///
/// For example "/contact/"
public void Navigate(string navigatePath)
{
Navigate(navigatePath, isAddHistory: true);
}
///
/// Add navigate command to queue to navigate to current request path. For example: "/" or "/about/".
///
public void Navigate()
{
Navigate(UtilServer.Context.Request.Path.Value);
}
private NamingConvention namingConventionFramework;
private NamingConvention namingConventionApp;
internal NamingConvention NamingConventionInternal(Type typeRow)
{
if (UtilDalType.TypeRowIsFrameworkDb(typeRow))
{
if (namingConventionFramework == null)
{
namingConventionFramework = new NamingConvention();
}
}
if (namingConventionApp == null)
{
namingConventionApp = NamingConventionApp();
}
return namingConventionApp;
}
internal async Task InitInternalAsync()
{
await InitAsync();
UtilServer.Session.SetString("Main", string.Format("App start: {0}", UtilFramework.DateTimeToString(DateTime.Now.ToUniversalTime())));
}
internal async Task ProcessInternalAsync(AppJson appJson, AppSelector appSelector)
{
UtilStopwatch.TimeStart("Process");
while (appJson.RequestJson.CommandGet() != null)
{
await UtilApp.ProcessHomeIsClickAsync(appJson);
await UtilApp.ProcessNavigatePostAsync(appJson, appSelector); // Link POST instead of GET.
await UtilGrid.ProcessAsync(appJson); // Process data grid.
await UtilApp.ProcessBootstrapNavbarAsync(appJson);
BulmaNavbar.ProcessAsync(appJson);
UtilApp.ProcessDialpadIsClick(appJson);
// Page ProcessAsync
foreach (var item in this.ComponentListAll())
{
if (item is Page page)
{
await page.ProcessAsync();
}
if (item is Html html)
{
await html.ProcessAsync();
}
}
appJson.RequestJson.CommandNext();
}
DivContainer.Render(appJson);
UtilApp.BootstrapNavbarRender(appJson);
BulmaNavbar.Render(appJson);
UtilStopwatch.TimeStop("Process");
}
///
/// Gets RequestJson. Payload of current request.
///
[Serialize(SerializeEnum.None)]
internal RequestJson RequestJson;
///
/// Gets or sets RequestCount. Used by client. Does not send new request while old is still pending.
///
internal int RequestCount { get; set; }
///
/// Gets ResponseCount. Used by server to verify incoming request matches last response.
///
internal int ResponseCount { get; set; }
///
/// Gets IsSessionExpired. If true, session expired and application has been recycled.
///
public bool IsSessionExpired { get; internal set; }
internal string Version { get; set; }
internal string VersionBuild { get; set; }
internal bool IsServerSideRendering { get; set; }
internal string Session { get; set; }
internal string SessionApp { get; set; }
///
/// Gets or sets IsReload. If true, client reloads page. This appens after exception has been thrown.
///
[Serialize(SerializeEnum.Client)]
internal bool IsReload { get; set; }
///
/// Gets or sets IsPageNotFound. If true, page is not serialized at the end of request.
///
internal bool IsPageNotFound { get; set; }
///
/// Gets RequestUrlHost. This value is set by the server. For example: http://localhost:5000/". Used by client for app.json post. See also method ;
///
internal string RequestUrlHost { get; set; }
///
/// Gets EmbeddedUrl. Value used by Angular client on first app.json POST to indicate application is embedded and running on other website.
///
internal string EmbeddedUrl { get; set; }
///
/// Gets or sets DownloadData Used to send file to download to client.. See also method .
///
[Serialize(SerializeEnum.Client)]
internal string DownloadData;
///
/// Gets or sets NavigatePathAddHistory. This path is added by the browser to the navigate history. For example: "/contact/" or "/signin/", if redirected.
///
[Serialize(SerializeEnum.Client)]
internal string NavigatePathAddHistory;
///
/// Gets or sets DownloadFileName. For example Grid.xlsx
///
[Serialize(SerializeEnum.Client)]
internal string DownloadFileName;
///
/// Gets or sets DownloadContentType. See also method UtilServer.ContentType();
///
[Serialize(SerializeEnum.Client)]
internal string DownloadContentType;
///
/// Send file with app.json response to download in client.
///
internal void Download(byte[] data, string fileName) // Used for Excel export.
{
this.DownloadData = Convert.ToBase64String(data);
this.DownloadFileName = fileName;
this.DownloadContentType = UtilServer.ContentType(fileName);
}
///
/// Gets or sets IsScrollToTop. Used for example for session expired.
///
[Serialize(SerializeEnum.Client)]
internal bool IsScrollToTop;
}
///
/// Json Button. Rendered as html button element. If user clicks it property IsClick is true.
///
public class Button : ComponentJson
{
public Button(ComponentJson owner)
: base(owner, nameof(Button))
{
}
///
/// Gets or sets TextHtml. Rendered by Angular as innerHtml.
///
public string TextHtml;
///
/// Gets IsClick. If true, user clicked the button.
///
public bool IsClick
{
get
{
var commandJson = ((AppJson)Root).RequestJson.CommandGet();
return commandJson.CommandEnum == CommandEnum.ButtonIsClick && commandJson.ComponentId == Id;
}
}
}
///
/// Json Div. Rendered as html div element.
///
public class Div : ComponentJson
{
public Div(ComponentJson owner)
: base(owner, nameof(Div))
{
}
///
/// Constructor used by derived DivContainer.
///
internal Div(ComponentJson owner, string type)
: base(owner, type)
{
}
///
/// Gets or sets TextHtml. Rendered by Angular as innerHtml.
///
public string TextHtml;
}
///
/// Renders div with child divs without Angular selector div in between. Used for example for css flexbox, css grid and Bootstrap row.
/// Children have to be of type Div.
///
public class DivContainer : Div
{
public DivContainer(ComponentJson owner)
: base(owner, nameof(DivContainer))
{
}
///
/// Remove non Div components from DivContainer.
///
internal static void Render(AppJson appJson)
{
foreach (var divContainer in appJson.ComponentListAll().OfType())
{
List listRemove = new List(); // Collect items to remove.
foreach (var item in divContainer.List)
{
if (!(item is Div)) // ComponentJson.Type is not evaluated on DivComponent children!
{
listRemove.Add(item);
}
}
foreach (var item in listRemove)
{
throw new Exception($"Child of DivContainer has to be a Div ({item.GetType().Name})!");
// item.ComponentRemove();
}
}
}
}
///
/// Data grid shows row as table, stack or form.
///
public class Grid : ComponentJson
{
///
/// Constructor.
///
public Grid(ComponentJson owner)
: base(owner, nameof(Grid))
{
this.Mode = GridMode.Table;
}
///
/// TypeRow of loaded data grid.
///
[Serialize(SerializeEnum.Session)]
internal Type TypeRow;
///
/// DatabaseEnum of loaded grid.
///
[Serialize(SerializeEnum.Session)]
internal DatabaseEnum DatabaseEnum;
///
/// Load data into grid. Override method Page.GridQuery(); to define query. It's also called to reload data.
///
public Task LoadAsync()
{
return UtilGrid.LoadAsync(this);
}
///
/// Gets or sets ConfigGrid. This is the current data grid configuration.
///
[Serialize(SerializeEnum.Session)]
internal FrameworkConfigGridIntegrate ConfigGrid;
///
/// Gets or sets ConfigFieldList. Can contain multiple configurations. See also property ConfigName.
///
[Serialize(SerializeEnum.Session)]
internal List ConfigFieldList;
///
/// Gets or sets RowListInternal. Data rows loaded from database.
///
[Serialize(SerializeEnum.Session)]
internal List RowListInternal; // TODO Remove empty RowListInternal from JsonClient.
///
/// Gets RowList. Data rows loaded from database.
///
public IReadOnlyList RowList
{
get
{
return RowListInternal;
}
}
///
/// Gets or sets ColumnList. Does not include hidden columns.
///
[Serialize(SerializeEnum.Session)]
internal List ColumnList;
///
/// Returns column width total starting at offset.
///
public double ColumnWidthTotal(int offset)
{
double result = 0;
for (int i = offset; i < ColumnList.Count; i++)
{
result += ColumnList[i].Width;
}
return result;
}
[Serialize(SerializeEnum.Session)]
internal List RowStateList;
///
/// Gets or sets GridCellList.
///
internal List CellList;
[Serialize(SerializeEnum.Session)]
internal List FilterValueList;
[Serialize(SerializeEnum.Session)]
internal List SortValueList;
[Serialize(SerializeEnum.Session)]
internal int OffsetRow;
[Serialize(SerializeEnum.Session)]
internal int OffsetColumn;
///
/// Gets or sets StyleColumnList. Contains for example column width.
///
internal List StyleColumnList;
///
/// Gets or sets StyleRowList. Used by Angular to iterate rows.
///
internal List StyleRowList;
///
/// Gets or sets IsHidePagination. If true, data grid pagination (and config button) is not shown.
///
internal bool IsHidePagination;
///
/// Gets or sets IsShowConfigDeveloper. If true, data grid config button is shown.
///
internal bool IsShowConfig;
///
/// Gets or sets IsShowConfigDeveloper. If true, config developer button (coffee icon) is shown to configure data grid.
///
internal bool IsShowConfigDeveloper;
///
/// Gets or sets IsGridLookup. If true, this grid is a lookup data grid.
///
[Serialize(SerializeEnum.Session)]
internal bool IsGridLookup;
///
/// Gets or sets GridLookup. Reference to lookup grid for this grid.
///
internal Grid GridLookup;
///
/// Gets or sets GridDest. If this data grid is a lookup grid, this is the destination data grid to write to after selection.
///
internal Grid GridDest;
///
/// Gets or sets GridLookupDestRowStateId. If this data grid is a lookup grid, this is the destination data row to write to after selection.
///
[Serialize(SerializeEnum.Session)]
internal int? GridLookupDestRowStateId;
///
/// Gets or sets GridLookupDestFieldNameCSharp. If this data grid is a lookup grid, this is the destination grid column (to write to) after selection.
///
[Serialize(SerializeEnum.Session)]
internal string GridLookupDestFieldNameCSharp;
///
/// Gets or sets RowSelect. Currently selected data row by user. Set queues command.
///
[Serialize(SerializeEnum.None)]
public Row RowSelect
{
get
{
Row result = null;
if (RowStateList != null)
{
foreach (var rowState in RowStateList)
{
if (rowState.IsSelect && rowState.RowEnum == GridRowEnum.Index)
{
result = RowListInternal[rowState.RowId.Value - 1];
break;
}
}
}
return result;
}
set
{
UtilGrid.QueueRowIsClick(this, value);
}
}
///
/// Gets RowSelectRowStateId. Currently selected data row by user.
///
internal int? RowSelectRowStateId
{
get
{
int? result = null;
foreach (var rowState in RowStateList)
{
if (rowState.IsSelect && rowState.RowEnum == GridRowEnum.Index)
{
result = rowState.Id;
break;
}
}
return result;
}
}
[Serialize(SerializeEnum.Session)]
internal GridMode Mode;
///
/// Returns query to load data grid. Override this method to define sql query.
///
/// If return value is null, grid has no header columns and no rows. If value is equal to method Data.QueryEmpty(); grid has header columns but no data rows.
/// If return value is true, first row is selected after data grid load.
internal virtual void QueryInternal(out IQueryable query, out bool isRowSelectFirst)
{
query = null;
isRowSelectFirst = false;
}
///
/// Override this method to reduce session state size. Truncate big data cells in grid. Typically, big data cells are opened individually on a separate page.
///
internal virtual void TruncateInternal(List rowList)
{
}
///
/// Override this method for custom implementation. Method is called when data row has been selected. Reload for example a detail data grid.
///
protected virtual internal Task RowSelectAsync()
{
return Task.FromResult(0);
}
protected virtual internal void CellParseFilter(string fieldName, string text, GridCellParseFilterResult result)
{
}
virtual internal Task UpdateInternalAsync(Row rowOld, Row row, FileUploadArgs fileUpload, DatabaseEnum databaseEnum, UpdateResultInternal result)
{
return Task.FromResult(0);
}
public class FileUploadArgs
{
///
/// Gets FieldName. User uploaded a file on this field.
///
public string FieldName { get; internal set; }
///
/// Gets Data. Contains data of uploaded file. Is null if no file has been uploaded.
///
public byte[] Data { get; internal set; }
///
/// Gets FileName. From user uploaded file.
///
public string FileName { get; internal set; }
}
internal class UpdateResultInternal
{
public bool IsHandled;
public static Grid.UpdateResult Convert(UpdateResultInternal value, TRow row) where TRow : Row
{
return new Grid.UpdateResult { Row = row, IsHandled = value.IsHandled };
}
public static void Convert(Grid.UpdateResult value, ref UpdateResultInternal result) where TRow : Row
{
result.IsHandled = value.IsHandled;
}
}
virtual internal Task InsertInternalAsync(Row row, FileUploadArgs fileUpload, DatabaseEnum databaseEnum, InsertResultInternal result)
{
return Task.FromResult(0);
}
internal class InsertResultInternal
{
public bool IsHandled;
public static Grid.InsertResult Convert(InsertResultInternal value, TRow row) where TRow : Row
{
return new Grid.InsertResult { Row = row, IsHandled = value.IsHandled };
}
public static void Convert(Grid.InsertResult value, ref InsertResultInternal result) where TRow : Row
{
result.IsHandled = value.IsHandled;
}
}
virtual internal string CellTextInternal(Row row, string fieldName, string text)
{
return text;
}
virtual internal void CellParseInternal(Row row, string fieldName, string text, ParseResultInternal result)
{
}
virtual internal Task CellParseInternalAsync(Row row, string fieldName, string text, ParseResultInternal result)
{
return Task.FromResult(0);
}
internal class ParseResultInternal
{
///
/// Gets or sets IsHandled. If true, framework does no further parsing of user entered text.
///
public bool IsHandled;
///
/// Gets or sets ErrorParse. For example: User entered text is not a number.
///
public string ErrorParse;
public static Grid.ParseResult Convert(ParseResultInternal value, TRow row) where TRow : Row
{
return new Grid.ParseResult { Row = row, ErrorParse = value.ErrorParse, IsHandled = value.IsHandled };
}
public static void Convert(Grid.ParseResult value, ref ParseResultInternal result) where TRow : Row
{
result.ErrorParse = value.ErrorParse;
result.IsHandled = value.IsHandled;
}
}
public enum CellAnnotationAlignEnum
{
///
/// None.
///
None = 0,
///
/// Align text left.
///
Left = 1,
///
/// Align data grid cell text in center .
///
Center = 2,
///
/// Align data grid cell text right.
///
Right = 3,
}
virtual internal void CellAnnotationInternal(GridRowEnum rowEnum, Row row, string fieldName, AnnotationResult result)
{
}
///
/// Provides additional annotation information for a data grid cell.
///
public class AnnotationResult
{
///
/// Gets or sets Html. If not null, cell is rendered as (readonly) html.
/// Used for example to transform plain text into a hyper link.
/// For multiline html set property IsMultiline.
/// For empty html set non-breaking space (nbsp) to keep the layout consistent with none empty html fields.
///
public string Html;
///
/// Gets or sets HtmlIsEdit. If true, html is rendered and additionally input text box is shown on top to edit plain html. Applies only if Html is not null.
///
public bool HtmlIsEdit;
///
/// Gets or sets HtmlLeft. Use for example to render an image on the left hand side in the cell.
///
public string HtmlLeft;
///
/// Gets or sets HtmlRight. Use for example to render an indicator icon on the right hand side in the cell.
///
public string HtmlRight;
///
/// Gets or sets IsReadOnly. If true, user can not edit text.
///
public bool IsReadOnly;
///
/// Gets or sets IsMultiline. If true, text box is multiline.
///
public bool IsMultiline;
///
/// Gets or sets IsPassword. If true, user can not read text.
///
public bool IsPassword;
///
/// Gets or sets IsFileUpload. If true, user can upload cell text (data) with file upload.
///
public bool IsFileUpload;
///
/// Gets or sets Placeholder. Shown as gray text when edit field is empty. For example: "Street" or "Search".
///
public string PlaceHolder;
///
/// Gets or sets Align. Defines text allign of centent in the data grid cell.
///
public CellAnnotationAlignEnum Align;
}
///
/// Arguments for config query.
///
public class QueryConfigArgs
{
///
/// Gets TableName. This is the TableName for which to return the config query. TableName as declared in CSharp code.
///
public string TableName { get; internal set; }
}
///
/// Returns one query for data grid configuration and one query for data grid field configuration.
///
public class QueryConfigResult
{
///
/// Gets or sets ConfigGrid. ConfigGrid without ConfigGridQuery.
///
public FrameworkConfigGridIntegrate ConfigGrid { get; set; }
///
/// Gets or sets ConfigGridQuery. Can return multiple configuration records but one record once filtered by ConfigName.
///
public IQueryable ConfigGridQuery { get; set; }
///
/// Gets or sets ConfigFieldQuery. Will be filtered (in memory) by ConfigName.
///
public IQueryable ConfigFieldQuery { get; set; }
///
/// Gets or sets ConfigName. Will be added to ConfigGridQuery and ConfigFieldQuery as additional (in memory) filter.
///
public string ConfigName { get; set; }
///
/// Gets or sets GridMode. This is the data grid display mode to start with. User can still switch later on.
///
public GridMode GridMode { get; set; }
}
///
/// Returns configuration query of data grid to load.
///
/// TableName as declared in CSharp code. Type of row to load.
internal QueryConfigResult QueryConfigInternal(string tableNameCSharp)
{
var result = new QueryConfigResult { GridMode = GridMode.Table }; // Display table by default
var args = new QueryConfigArgs { TableName = tableNameCSharp };
QueryConfig(args, result);
// Result returned one ConfigGrid instead of a query.
if (result.ConfigGrid != null)
{
result.ConfigGrid.TableNameCSharp = tableNameCSharp;
var configGridList = new List();
configGridList.Add(result.ConfigGrid);
result.ConfigGridQuery = configGridList.AsQueryable();
}
// Default result
if (result.ConfigGridQuery == null)
{
result.ConfigGridQuery = Data.Query().Where(item => item.TableNameCSharp == tableNameCSharp);
}
// Default result
if (result.ConfigFieldQuery == null)
{
result.ConfigFieldQuery = Data.Query().Where(item => item.TableNameCSharp == tableNameCSharp);
}
return result;
}
///
/// Returns configuration query of data grid to load.
///
protected virtual void QueryConfig(QueryConfigArgs args, QueryConfigResult result)
{
// Example for static configuration:
// result.ConfigGridQuery = new [] { new FrameworkConfigGridIntegrate { RowCountMax = 2 } }.AsQueryable();
}
virtual internal IQueryable LookupQueryInternal(Row row, string fieldName, string text)
{
return null; // No lookup data grid.
}
///
/// Returns configuration query of lookup data grid to load.
///
/// Lookup data grid for which to load the configuration.
/// TableName as declared in CSharp code.
internal QueryConfigResult LookupQueryConfigInternal(Grid gridLookup, string tableName)
{
var result = new QueryConfigResult();
var args = new QueryConfigArgs { TableName = tableName };
// Default result
result.ConfigGridQuery = Data.Query().Where(item => item.TableNameCSharp == tableName);
result.ConfigFieldQuery = Data.Query().Where(item => item.TableNameCSharp == tableName);
LookupQueryConfig(args, result);
// Filter one configuration
result.ConfigGridQuery = result.ConfigGridQuery.Where(item => item.ConfigName == result.ConfigName);
result.ConfigFieldQuery = result.ConfigFieldQuery.Where(item => item.ConfigName == result.ConfigName);
return result;
}
///
/// Returns configuration query of lookup data grid to load.
///
protected virtual void LookupQueryConfig(QueryConfigArgs args, QueryConfigResult result)
{
// Example for static configuration:
// result.ConfigGridQuery = new [] { new FrameworkConfigGridIntegrate { RowCountMax = 2 } }.AsQueryable();
}
///
/// Override this method to extract and return text from lookup grid row for further processing.
/// Process wise there is no difference between user selecting a row on the lookup grid or entering text manually.
/// Returns text like entered by user for further processing.
protected virtual internal void LookupRowSelect(LookupRowSelectArgs args, LookupRowSelectResult result)
{
}
public class LookupRowSelectArgs
{
///
/// Gets RowSelect. This is the row which has been clicked by the user in the lookup window.
///
public Row RowSelect { get; internal set; }
///
/// Gets FieldName. This is the FieldName for which the lookup window is open.
///
public string FieldName { get; internal set; }
}
public class LookupRowSelectResult
{
///
/// Gets or sets Text. Like the text entered by user for further processing.
///
public string Text { get; set; }
}
}
internal class GridStyleColumn
{
///
/// Gets or sets Width. This is the data grid column width. For example 33% or 33px.
///
public string Width;
///
/// Gets or sets WidthValue. For example 33.
///
public double? WidthValue;
///
/// Gets or sets WidthUnit. For example % or px.
///
public string WidthUnit;
}
///
/// Used by Angular to iterate the rows.
///
internal class GridStyleRow
{
}
public class GridCellParseFilterResult
{
public GridCellParseFilterResult(GridFilter gridFilter)
{
this.GridFilter = gridFilter;
}
public readonly GridFilter GridFilter;
public bool IsHandled;
public string ErrorParse;
}
public class Grid : Grid where TRow : Row
{
public Grid(ComponentJson owner)
: base(owner)
{
}
internal override void QueryInternal(out IQueryable query, out bool isRowSelectFirst)
{
QueryArgs args = new QueryArgs();
QueryResult result = new QueryResult { IsRowSelectFirst = true };
// Custom query
Query(args, result);
// Default query, if no custom query provided.
if (result.Query == null)
{
result.Query = args.Query;
}
query = result.Query;
isRowSelectFirst = result.IsRowSelectFirst;
}
///
/// Returns query to load data grid.
///
protected virtual void Query(QueryArgs args, QueryResult result)
{
}
public class QueryArgs
{
///
/// Gets Query. This is the default query.
///
public IQueryable Query
{
get
{
if (typeof(TRow) == typeof(Row))
{
return null; // Data.QueryEmpty(); is not possible since class Row has no TableNameSql defined.
}
else
{
return Data.Query();
}
}
}
}
public class QueryResult
{
///
/// Gets or sets Query. Query used to load data grid.
/// If value is null, grid has no header columns and no rows. If value is equal to method Data.QueryEmpty(); grid has header columns but no data rows.
///
public IQueryable Query { get; set; }
///
/// Gets or sets IsRowSelectFirst. If true, first row is selected after data grid load.
///
public bool IsRowSelectFirst { get; set; }
}
internal override void TruncateInternal(List rowList)
{
foreach (var row in rowList)
{
Truncate(new TruncateArgs { Row = (TRow)row });
}
}
///
/// Override this method to reduce session state size. Truncate big data cells in grid. Typically, big data cells are opened individually on a separate page.
///
protected virtual void Truncate(TruncateArgs args)
{
}
public class TruncateArgs
{
///
/// Gets Row. Truncate big data cells to reduce session state size.
///
public TRow Row { get; internal set; }
}
internal override async Task UpdateInternalAsync(Row rowOld, Row row, FileUploadArgs fileUpload, DatabaseEnum databaseEnum, UpdateResultInternal result)
{
UpdateResult resultLocal = UpdateResultInternal.Convert(result, (TRow)row);
await UpdateAsync(new UpdateArgs { RowOld = (TRow)rowOld, Row = (TRow)row, FileUpload = fileUpload, DatabaseEnum = databaseEnum }, resultLocal);
UpdateResultInternal.Convert(resultLocal, ref result);
}
///
/// Override this method for custom grid save implementation. Return isHandled.
///
/// Returns true, if custom save was handled. If false, framework will handle update.
protected virtual Task UpdateAsync(UpdateArgs args, UpdateResult result)
{
return Task.FromResult(0);
}
public class UpdateArgs
{
///
/// Gets RowOld. Data row with old data to update.
///
public TRow RowOld { get; internal set; }
///
/// Gets Row. New data row to save to database.
///
public TRow Row { get; internal set; }
///
/// Gets FileUpload. Contains all data if user sent a file. Null if no file has been uploaded with current request.
///
public FileUploadArgs FileUpload { get; internal set; }
public DatabaseEnum DatabaseEnum { get; internal set; }
}
public class UpdateResult
{
///
/// Gets or sets IsHandled. If true, framework does not update data row.
///
public bool IsHandled;
///
/// Gets Row. New data row to save to database.
///
public TRow Row { get; internal set; }
}
internal override async Task InsertInternalAsync(Row row, FileUploadArgs fileUpload, DatabaseEnum databaseEnum, InsertResultInternal result)
{
InsertResult resultLocal = InsertResultInternal.Convert(result, (TRow)row);
var fileUploadArgs = fileUpload.Data != null ? fileUpload : null;
await InsertAsync(new InsertArgs { Row = (TRow)row, FileUpload = fileUploadArgs, DatabaseEnum = databaseEnum }, resultLocal);
InsertResultInternal.Convert(resultLocal, ref result);
}
///
/// Override this method for custom grid save implementation. Returns isHandled.
///
/// Returns true, if custom save was handled.
protected virtual Task InsertAsync(InsertArgs args, InsertResult result)
{
return Task.FromResult(0);
}
public class InsertArgs
{
///
/// Gets Row. Data row to insert. Set new primary key on this row.
///
public TRow Row { get; internal set; }
///
/// Gets FileUpload. Contains all data if user sent a file. Null if no file has been uploaded with current request.
///
public FileUploadArgs FileUpload { get; internal set; }
public DatabaseEnum DatabaseEnum { get; internal set; }
}
public class InsertResult
{
///
/// Gets or sets IsHandled. If true, framework does not insert data row.
///
public bool IsHandled;
///
/// Gets Row. Data row to insert. Set new primary key on this row.
///
public TRow Row { get; internal set; }
}
///
/// Gets RowList. Data rows loaded from database.
///
public new IReadOnlyList RowList
{
get
{
return base.RowListInternal.Cast().ToList();
}
}
///
/// Gets RowSelect. Currently selected data row by user. Set queues command.
///
[Serialize(SerializeEnum.None)]
public new TRow RowSelect
{
get
{
return (TRow)base.RowSelect;
}
set
{
base.RowSelect = value;
}
}
internal override string CellTextInternal(Row row, string fieldName, string text)
{
var args = new CellTextArgs { Row = (TRow)row, FieldName = fieldName, Text = text };
var result = new CellTextResult { Text = text };
CellText(args, result);
return result.Text;
}
///
/// Override this method for custom implementation of converting database value to front end grid cell text. Called only if database value is not null.
///
protected virtual void CellText(CellTextArgs args, CellTextResult result)
{
}
public class CellTextArgs
{
///
/// Data grid row.
///
public TRow Row { get; internal set; }
///
/// FieldName as declared in CSharp code. Data grid column name.
///
public string FieldName { get; internal set; }
///
/// Default database value front end grid cell text.
///
public string Text { get; internal set; }
}
public class CellTextResult
{
///
/// Custom database value front end grid cell text.
///
public string Text;
}
internal override void CellParseInternal(Row row, string fieldName, string text, ParseResultInternal result)
{
var resultLocal = ParseResultInternal.Convert(result, (TRow)row);
CellParse(new ParseArgs { Row = (TRow)row, FieldName = fieldName, Text = text }, resultLocal);
ParseResultInternal.Convert(resultLocal, ref result);
}
///
/// Parse user entered text and assign it row. Write parsed value to row. (Or for example multiple fields on row for Uom)
///
/// Set result.IsHandled to true.
protected virtual void CellParse(ParseArgs args, ParseResult result)
{
}
public class ParseArgs
{
///
/// Write custom parsed value to row.
///
public TRow Row { get; internal set; }
///
/// FieldName as declared in CSharp code. Data grid column name.
///
public string FieldName { get; internal set; }
///
/// User entered text. It can be empty but never null.
///
public string Text { get; internal set; }
}
public class ParseResult
{
///
/// Write custom parsed value to row.
///
public TRow Row { get; internal set; }
///
/// Gets or sets IsHandled. If true, framework does no further parsing of user entered text.
///
public bool IsHandled;
///
/// Gets or sets ErrorParse. For example: User entered text is not a number.
///
public string ErrorParse;
}
internal override async Task CellParseInternalAsync(Row row, string fieldName, string text, ParseResultInternal result)
{
var resultLocal = ParseResultInternal.Convert(result, (TRow)row);
await CellParseAsync(new ParseArgs { Row = (TRow)row, FieldName = fieldName, Text = text }, resultLocal);
ParseResultInternal.Convert(resultLocal, ref result);
}
///
/// Parse text user entered in cell and write it into parameter 'row'.
///
/// Return isHandled. If true, framework does no further parsing of user entered text.
///
protected virtual Task CellParseAsync(ParseArgs args, ParseResult result)
{
return Task.FromResult(0);
}
internal override void CellAnnotationInternal(GridRowEnum rowEnum, Row row, string fieldName, AnnotationResult result)
{
if (rowEnum == GridRowEnum.Index)
{
var args = new AnnotationArgs { Row = (TRow)row, FieldName = fieldName };
CellAnnotation(args, result);
}
else
{
var args = new AnnotationFilterNewArgs { Row = (TRow)row, FieldName = fieldName };
args.IsFilter = rowEnum == GridRowEnum.Filter;
args.IsNew = rowEnum == GridRowEnum.New;
CellAnnotationFilterNew(args, result);
}
}
///
/// Override this method to provide additional custom annotation information for a data grid cell. Annotation is updated for every cell on same row when user changes text in one cell.
///
/// Returns data grid cell annotation.
protected virtual void CellAnnotation(AnnotationArgs args, AnnotationResult result)
{
}
///
/// Annotation for data row.
///
public class AnnotationArgs
{
///
/// Data grid row. Null, if filter or new data row.
///
public TRow Row { get; internal set; }
///
/// FieldName as declared in CSharp code. Data grid column name.
///
public string FieldName { get; internal set; }
}
public class AnnotationFilterNewArgs
{
///
/// Data grid row. Null, if filter or new data row.
///
public TRow Row { get; internal set; }
///
/// FieldName as declared in CSharp code. Data grid column name.
///
public string FieldName { get; internal set; }
///
/// Gets IsFilter. If true, annotation is for filter row.
///
public bool IsFilter { get; internal set; }
///
/// Gets IsNew. If true, annotation is for new row.
///
public bool IsNew { get; internal set; }
}
///
/// Override this method to provide annotation information for data grid cell in filter and new row.
///
/// Returns data grid cell annotation.
protected virtual void CellAnnotationFilterNew(AnnotationFilterNewArgs args, AnnotationResult result)
{
}
internal override IQueryable LookupQueryInternal(Row row, string fieldName, string text)
{
LookupQueryResult result = new LookupQueryResult();
LookupQuery(new LookupQueryArgs { Row = (TRow)row, FieldName = fieldName, Text = text }, result);
return result.Query;
}
///
/// Override this method to return a linq query for the lookup data grid.
///
/// Returns query for lookup window.
protected virtual void LookupQuery(LookupQueryArgs args, LookupQueryResult result)
{
}
public class LookupQueryArgs
{
///
/// Gets Row. This is the row user is editing.
///
public TRow Row { get; internal set; }
///
/// Gets FieldName. As declared in CSharp code. This is the field the user is editing.
///
public string FieldName { get; internal set; }
///
/// Gets Text. This is the text the user entered.
///
public string Text { get; set; }
}
public class LookupQueryResult
{
///
/// Gets or sets Query. This is the query for the lookup window.
///
public IQueryable Query { get; set; }
}
}
///
/// Data grid display mode.
///
public enum GridMode
{
None = 0,
///
/// Display grid cells as table.
///
Table = 1,
///
/// Display grid cells stacked on top of each other.
///
Stack = 2,
///
/// Display grid cells in predifined positions.
///
Form = 3
}
///
/// Wrapper providing filter value store functions.
///
public sealed class GridFilter
{
internal GridFilter(Grid grid)
{
this.Grid = grid;
}
internal readonly Grid Grid;
///
/// Returns filter value for field.
///
private GridFilterValue FilterValue(string fieldNameCSharp)
{
GridFilterValue result = Grid.FilterValueList.Where(item => item.FieldNameCSharp == fieldNameCSharp).SingleOrDefault();
if (result == null)
{
result = new GridFilterValue(fieldNameCSharp);
Grid.FilterValueList.Add(result);
}
return result;
}
///
/// Set filter value on a column. If text is not equal to text user entered, it will appear as soon as user leves field.
///
/// If true, filter is not applied.
public void ValueSet(string fieldNameCSharp, object filterValue, FilterOperator filterOperator, string text, bool isClear = false)
{
GridFilterValue result = FilterValue(fieldNameCSharp);
result.FilterValue = filterValue;
result.FilterOperator = filterOperator;
if (result.IsFocus == false)
{
result.Text = text;
}
else
{
result.TextLeave = text;
}
result.IsClear = isClear;
}
internal void TextSet(string fieldNameCSharp, string text)
{
Grid.FilterValueList.ForEach(item => item.IsFocus = false);
GridFilterValue result = FilterValue(fieldNameCSharp);
result.Text = text;
result.IsFocus = true;
}
///
/// (FieldNameCSharp, FilterValue).
///
internal Dictionary FilterValueList()
{
var result = new Dictionary();
if (Grid.FilterValueList != null)
{
foreach (var item in Grid.FilterValueList)
{
result.Add(item.FieldNameCSharp, item);
}
}
return result;
}
}
///
/// Stores successfully parsed filter value and operator.
///
internal sealed class GridFilterValue
{
public GridFilterValue(string fieldNameCSharp)
{
this.FieldNameCSharp = fieldNameCSharp;
}
public readonly string FieldNameCSharp;
public FilterOperator FilterOperator;
///
/// Gets or sets FilterValue. This is the successfully parsed user input value.
///
public object FilterValue;
///
/// Gets or sets IsClear. If true, filter has been cleared and is not applied.
///
public bool IsClear;
///
/// Gets or sets Text of successfully parsed filter.
///
public string Text;
///
/// Gets or sets TextLeave. If filter has user input focus, parser can not override text untill user leaves the field.
///
public string TextLeave;
///
/// Gets or sets IsFocus. If true, filter has user input focus.
///
public bool IsFocus;
}
internal sealed class GridSortValue
{
public GridSortValue(string fieldNameCSharp)
{
this.FieldNameCSharp = fieldNameCSharp;
}
public readonly string FieldNameCSharp;
public bool IsSort;
public static bool? IsSortGet(Grid grid, string fieldNameCSharp)
{
bool? result = null;
var value = grid.SortValueList?.FirstOrDefault();
if (value != null && value.FieldNameCSharp == fieldNameCSharp)
{
result = value.IsSort;
}
return result;
}
public static void IsSortSwitch(Grid grid, string fieldNameCSharp)
{
var value = grid.SortValueList.FirstOrDefault();
if (value != null && value.FieldNameCSharp == fieldNameCSharp)
{
value.IsSort = !value.IsSort; // Switch order
}
else
{
grid.SortValueList.Insert(0, new GridSortValue(fieldNameCSharp) { IsSort = false });
}
while (grid.SortValueList.Count > 2) // Order by then order by (max two levels).
{
grid.SortValueList.RemoveAt(grid.SortValueList.Count - 1);
}
}
}
///
/// Not sent to client.
///
internal sealed class GridColumn
{
public int Id;
public string FieldNameCSharp;
///
/// Gets or sets ColumnText. This is the header text for filter.
///
public string ColumnText;
///
/// Gets or sets Description. Shown with an information icon in header.
///
public string Description;
///
/// Gets or sets IsVisible. If true, column is shown in data grid.
///
public bool IsVisible;
///
/// Gets or sets IsReadOnly.
///
public bool IsReadOnly;
///
/// Gets or sets Width. Value is By default 1.
///
public double Width;
///
/// Gets or sets IsMultiline.
///
public bool IsMultiline;
public bool IsVisibleScroll;
///
/// Gets or sets Sort. Order as defined in data grid field config.
///
public double? Sort;
///
/// Gets or sets SortField. Order as defined in sql database schema.
///
public int SortField;
///
/// Gets or sets WidthValue. For example 33%.
///
public double? WidthValue;
}
///
/// Keeps track of data row state. Not sent to client.
///
internal sealed class GridRowState
{
public int Id;
public GridRowEnum RowEnum;
public int? RowId; // Filter does not have a data row.
///
/// Gets or sets IsSelect. User clicked and selected this data row.
///
public bool IsSelect;
///
/// Gets or sets IsVisibleScroll. For vertical paging (no database select).
///
public bool IsVisibleScroll;
///
/// Gets or sets Row. Data row to update (index) or insert (new) into database.
///
public Row Row;
}
internal enum GridCellEnum
{
None = 0,
///
/// Data grid filter cell.
///
Filter = 1,
///
/// Data grid cell.
///
Index = 2,
///
/// Data grid cell.
///
New = 3,
///
/// Column header with IsSort.
///
HeaderColumn = 4,
///
/// Cell label in stack mode.
///
HeaderRow = 5,
///
/// Separator label in stack mode.
///
Separator = 6,
}
///
/// Grid cell display sent to client. Unlike GridColumn a cell it is not persistent and lives only while it is IsVisibleScroll or contains ErrorParse.
///
internal sealed class GridCell : IHide
{
///
/// Gets or sets Id. Sent back by client with .
///
public int Id;
[Serialize(SerializeEnum.Session)]
public int ColumnId;
[Serialize(SerializeEnum.Session)]
public int RowStateId;
public GridCellEnum CellEnum;
///
/// Gets or sets ColumnText. Header for Filter.
///
public string ColumnText;
///
/// Gets or sets json text. Can be null but never empty.
///
public string Text;
///
/// Gets or sets TextLeave. If not null, client writes TextLeave into cell if focus is lost. This prevents overriding text while user is editing cell. Can be null or empty.
///
public string TextLeave;
///
/// Gets or sets TextOld. This is the text before save.
///
[Serialize(SerializeEnum.Session)]
public string TextOld;
///
/// Gets IsModified. If true, user changed text.
///
[Serialize(SerializeEnum.Session)]
public bool IsModified;
///
/// Gets or sets ErrorParse. Text user entered could not be parsed and written to row.
///
public string ErrorParse;
///
/// Gets or sets ErrorSave. Row could not be saved to the database.
///
public string ErrorSave;
public string Warning;
public string Placeholder;
public string Description;
///
/// Gets or sets IsSelect. If true, cell belongs to selected row.
///
public bool IsSelect;
///
/// Gets or sets IsSort. Display column sort triangle.
///
public bool? IsSort;
///
/// Gets or sets IsVisibleScroll. If true, cell is visible in scrallable range. If false, cell is not sent to client.
///
[Serialize(SerializeEnum.Session)]
public bool IsVisibleScroll;
///
/// Gets or sets IsHide. Calculated property for interface IHide.
///
[Serialize(SerializeEnum.None)]
public bool IsHide
{
get
{
return !IsVisibleScroll;
}
set
{
IsVisibleScroll = !value;
}
}
///
/// Gets or sets GridLookup.
///
[Serialize(SerializeEnum.Both)] // By default, reference to ComponentJson is not sent to client. Serialize grid to client exclusively. JsonSession serializes it as reference.
public Grid GridLookup;
///
/// Gets or sets Html. Use for example to transform plain text into a hyper link.
///
public string Html;
///
/// Gets or sets HtmlIsEdit. If true, html is rendered and additionally input text box is shown to edit plain html. Applies only if Html is not null.
///
public bool HtmlIsEdit;
///
/// Gets or sets HtmlLeft. Use for example to render an image on the left hand side in the cell.
///
public string HtmlLeft;
///
/// Gets or sets HtmlRight. Use for example to render an indicator icon on the right hand side in the cell.
///
public string HtmlRight;
///
/// Gets or sets IsReadOnly. If true, user can not edit text.
///
public bool IsReadOnly;
///
/// Gets or sets IsMultiline. If true, text box is multiline.
///
public bool IsMultiline;
///
/// Gets or sets IsPassword. If true, user can not read text.
///
public bool IsPassword;
///
/// Gets or sets Align. Defines text allign of centent in the data grid cell.
///
public Grid.CellAnnotationAlignEnum Align;
///
/// Gets or sets IsFileUpload. If true, user can upload cell text (data) with file upload.
///
public bool IsFileUpload;
///
/// Gets or sets IsOdd.
///
public bool IsOdd;
}
///
/// Grid paging.
///
public enum GridIsClickEnum
{
None = 0,
///
/// Page up and load data rows from database.
///
PageUp = 1,
///
/// Page down and load data rows from database.
///
PageDown = 2,
///
/// Page (scroll) left and show new cells in view. No data row load from database.
///
PageLeft = 3,
///
/// Page (scroll) right and show new cells in view. No data row load from database.
///
PageRight = 4,
///
/// Show data grid in table mode.
///
ModeTable = 7,
///
/// Show data grid in stack mode.
///
ModeStack = 8,
///
/// Show data grid in form mode.
///
ModeForm = 9,
///
/// Download data rows as Excel (*.xlsx) file.
///
ExcelDownload = 10,
///
/// Upload data rows as Excel (*.xlsx) file.
///
ExcelUpload = 11,
///
/// Clear filter and reload data rows from database.
///
Reload = 5,
///
/// Open data grid config dialog.
///
Config = 6,
///
/// Open data grid config dialog for developer. User clicked grid (coffee icon).
///
ConfigDeveloper = 12,
}
public class Html : ComponentJson
{
public Html(ComponentJson owner)
: base(owner, nameof(Html))
{
}
///
/// Gets or sets TextHtml. Rendered by Angular as innerHtml.
///
public string TextHtml;
///
/// Gets or sets IsNoSanatize. If true, Angular does not sanatize TextHtml. Html elements such as input and button are shown and onclick script executed. Html element script is NOT executed. See also property IsNoSanatizeScript.
///
public bool IsNoSanatize;
///
/// Gets or sets IsNoSanatizeScript. This property contains an html script. This script is executed always after TextHtml has been updated. Used for example for prismjs Prism.highlightAll();
///
public string IsNoSanatizeScript;
///
/// Returns true if user clicked button in this Html json component.
///
/// Html element id of button. Use it to distinct if multiple buttons.
public bool ButtonIsClick(string id = null)
{
var result = false;
if (IsRemoved == false)
{
var command = this.ComponentOwner().RequestJson.CommandGet();
result = command.CommandEnum == CommandEnum.HtmlButtonIsClick && command.ComponentId == this.Id;
if (result && id != null)
{
result = command.HtmlButtonId == id;
}
}
return result;
}
///
/// Override this method to implement custom process at the end of the process chain. Called once every request.
/// For example to process html button click.
///
protected virtual internal Task ProcessAsync()
{
return Task.FromResult(0);
}
}
public class Page : ComponentJson
{
///
/// Constructor. Use method PageShowAsync(); to create new page.
///
public Page(ComponentJson owner)
: base(owner, nameof(Page))
{
}
///
/// Called once a lifetime when page is created.
///
public virtual Task InitAsync()
{
return Task.FromResult(0);
}
///
/// Override this method to implement custom process at the end of the process chain. Called once every request.
///
protected virtual internal Task ProcessAsync()
{
return Task.FromResult(0);
}
}
public enum AlertEnum
{
None = 0,
Info = 1,
Success = 2,
Warning = 3,
Error = 4
}
///
/// Application feedback message.
///
public class Alert : Html
{
private Alert(ComponentJson owner, string textHtml, AlertEnum alertEnum, int index)
: base(owner)
{
var settingEnum = this.ComponentOwner().CssFrameworkEnum;
switch (settingEnum)
{
case CssFrameworkEnum.Bootstrap:
{
// See also: https://getbootstrap.com/docs/4.4/components/alerts/
string textHtmlTemplate = "
{{TextHtml}}
";
string cssClass = null;
switch (alertEnum)
{
case AlertEnum.Info:
cssClass = "alert-info";
break;
case AlertEnum.Success:
cssClass = "alert-success";
break;
case AlertEnum.Warning:
cssClass = "alert-warning";
break;
case AlertEnum.Error:
cssClass = "alert-danger";
break;
default:
break;
}
textHtmlTemplate = textHtmlTemplate.Replace("{{CssClass}}", cssClass).Replace("{{TextHtml}}", textHtml);
TextHtml = textHtmlTemplate;
IsNoSanatize = true;
}
break;
case CssFrameworkEnum.Bulma:
{
// See also: https://bulma.io/documentation/elements/notification/
string textHtmlTemplate = "
{{TextHtml}}
";
string cssClass = null;
switch (alertEnum)
{
case AlertEnum.Info:
cssClass = "notification is-info";
break;
case AlertEnum.Success:
cssClass = "notification is-success";
break;
case AlertEnum.Warning:
cssClass = "notification is-warning";
break;
case AlertEnum.Error:
cssClass = "notification is-danger";
break;
default:
break;
}
textHtmlTemplate = textHtmlTemplate.Replace("{{CssClass}}", cssClass).Replace("{{TextHtml}}", textHtml);
TextHtml = textHtmlTemplate;
IsNoSanatize = true;
}
break;
default:
throw new Exception("Enum unknown!");
}
// Move to top
this.ComponentMove(index);
}
///
/// Constructor. Shows message and scrolls browser to top.
///
public Alert(AppJson appJson, string textHtml, AlertEnum alertEnum)
: this(appJson.AlertContainerGet(), textHtml, alertEnum, 0)
{
appJson.IsScrollToTop = true;
}
protected internal override Task ProcessAsync()
{
if (ButtonIsClick())
{
this.ComponentRemove();
}
return base.ProcessAsync();
}
}
///
/// Application dialog.
///
public class PageModal : Page
{
public PageModal(ComponentJson owner) : base(owner)
{
var settingEnum = this.ComponentOwner().CssFrameworkEnum;
switch (settingEnum)
{
case CssFrameworkEnum.Bootstrap:
{
// https://getbootstrap.com/docs/4.4/components/modal/
new Div(this) { CssClass = "modal-backdrop show" };
var divModal = new Div(this) { CssClass = "modal" };
var divModalDialog = new Div(divModal) { CssClass = "modal-dialog" };
var divModalContent = new Div(divModalDialog) { CssClass = "modal-content" };
var divHeader = new Div(divModalContent) { CssClass = "modal-header" };
DivHeader = new Div(divHeader);
ButtonClose = new Button(divHeader) { CssClass = "close", TextHtml = "×" };
DivBody = new Div(divModalContent) { CssClass = "modal-body" };
DivFooter = new Div(divModalContent) { CssClass = "modal-footer" };
divModalDialog.CssClass += " modal-lg";
}
break;
case CssFrameworkEnum.Bulma:
{
// See also: https://bulma.io/documentation/elements/notification/
var divModal = new DivContainer(this) { CssClass = "modal is-active" };
new Div(divModal) { CssClass = "modal-background" };
var divCard = new Div(divModal) { CssClass = "modal-card modal-bulma-framework" }; // Override Bulma modal window width.
var divHeaderLocal = new DivContainer(divCard) { CssClass = "modal-card-head" };
DivHeader = new Div(divHeaderLocal) { CssClass = "modal-card-title" };
ButtonClose = new Button(new Div(divHeaderLocal)) { CssClass = "delete" };
DivBody = new Div(divCard) { CssClass = "modal-card-body" };
DivFooter = new Div(divCard) { CssClass = "modal-card-foot" };
// Title
{
// new Html(result.DivHeader) { TextHtml = "
Title
" };
}
// Two buttons in Html
{
// new Html(result.DivFooter) { TextHtml = "", IsNoSanatize = true };
}
// Two individual buttons
{
// new Button(result.DivFooter) { CssClass = "button is-success", TextHtml = "Ok" };
// new Html(result.DivFooter) { TextHtml = " " };
// new Button(result.DivFooter) { CssClass = "button", TextHtml = "Cancel" };
}
}
break;
default:
throw new Exception("Enum unknown!");
}
}
public Div DivHeader;
public Div DivBody;
public Div DivFooter;
public Button ButtonClose;
protected internal override Task ProcessAsync()
{
if (ButtonClose.IsClick)
{
this.ComponentRemove();
}
return base.ProcessAsync();
}
}
///
/// Dialpad with numbers.
///
public class Dialpad : ComponentJson
{
///
/// Constructor.
///
public Dialpad(ComponentJson owner)
: base(owner, nameof(Dialpad))
{
}
///
/// Gets or sets Text. This is the numbers entered by user.
///
public string Text;
}
///
/// Custom component. For example for footer component.
///
public class Custom01 : ComponentJson
{
///
/// Constructor.
///
public Custom01(ComponentJson owner)
: base(owner, nameof(Custom01))
{
}
///
/// Gets or sets TextHtml. Rendered by Angular as innerHtml.
///
public string TextHtml;
}
///
/// Custom component. For example for footer component.
///
public class Custom02 : ComponentJson
{
///
/// Constructor.
///
public Custom02(ComponentJson owner)
: base(owner, nameof(Custom02))
{
}
///
/// Gets or sets TextHtml. Rendered by Angular as innerHtml.
///
public string TextHtml;
}
///
/// Custom component. For example for footer component.
///
public class Custom03 : ComponentJson
{
///
/// Constructor.
///
public Custom03(ComponentJson owner)
: base(owner, nameof(Custom03))
{
}
///
/// Gets or sets TextHtml. Rendered by Angular as innerHtml.
///
public string TextHtml;
}
}