Important
as of 2024-03-08 I'm not going to maintain this project anymore
Philadelphia Toolkit is an integrated aspnet core and Bridge.NET cross platform toolkit for building type safe web applications in C#.
On the server side it is possible to use other dotnet languages (fact: most of our server side code is written in F#). Code may be shared between server and client (i.e. service contracts, DTOs etc). We strive to keep code concise yet without sacrificing possibility to structure code when it grows. One of the benefits provided by toolkit is that eliminates the need to explicitly invoke JSON serialization/deserialization code (both on server side and client side).
Extensive widgets collection: it provides widgets primarily targeting desktop browsers as its origin lays in LOB apps. We recently added support for mobile browsers by supporting Industrial Android WebApp Host which is webview with additional APIs for photo taking and QR scanning.
Server-sent events - it makes development of services utilizing this tech easy by handling low level concerns away (serialization, deserialization, subscription, canceling subscription, timeouts).
Why calling it toolkit instead of framework? Toolkit assumes that we are helping build tools instead of confining people in 'one true rigid and opinionated' limitations of framework.
See Philadelphia.Demo project hosted live on Linux within Docker. Please note that this is cheapest Linux machine available so it is likely that significant demand may cause it to be unavailable due to hug of death.
If you open it with browser it assumes desktop mode. If you open it with Industrial Android WebApp Host it assumes mobile browser.
Technically speaking you only need dotnet core SDK v2.1 or later installed and dotnet framework 4.5.1 (under Linux you need recent Mono). Why both? Because as of now for compilation of (Bridge v17.10.1) you need 'full dotnet framework'. For runtime it only needs dotnet core.
Install nuget template package:
dotnet new -i Philadelphia.Template
Use that template to create new solution in current directory:
dotnet new phillyapp -n PutYourProjectNameHere
IMPORTANT After using template for the first time you need to perform following actions (otherwise Bridge.NET won't generate JS files):
- Open generated *.sln file in Visual Studio
- right click on *.Server project and choose 'Set as StartUp project'
- run Build->Clean Solution
- run Build->Rebuild Solution
- quit Visual Studio
- reopen sln in Visual Studio
BUT WHY? This is due to csproj referencing files provided by nuget and NOT present during sln opening. If you won't do it then JS files won't be generated in output folder...
Create new solution as described in Installation section and replace Program.cs content as generated by template with following content (or open Examples/01HelloWorld/HelloWorld.sln):
using Bridge;
using Philadelphia.Web;
namespace PutYourProjectNameHere.Client {
public class Program {
[Ready]
public static void OnReady() {
Toolkit.InitializeToolkit();
var renderer = Toolkit.DefaultFormRenderer();
var msg = new InformationalMessageForm("Hello world");
msg.Ended += (x, _) => renderer.Remove(x);
renderer.AddPopup(msg);
}
}
}
above yields following outcome in Firefox
Also present under open Examples/02Navigation/Navigation.sln
using System;
using Bridge;
using Bridge.Html5;
using Philadelphia.Common;
using Philadelphia.Web;
namespace PhiladelphiaPowered.Client {
class MainFormView : IFormView<HTMLElement> {
public InputTypeButtonActionView ShowInfo {get; }
= new InputTypeButtonActionView("Info popup");
public InputTypeButtonActionView ReplaceMaster {get; }
= new InputTypeButtonActionView("Replace master");
public IView<HTMLElement>[] Actions => ActionsBuilder.For(ShowInfo,ReplaceMaster); //shorter than explicit array
public RenderElem<HTMLElement>[] Render(HTMLElement parentContainer) {
return new RenderElem<HTMLElement>[] {"this is main form body<br>using <i>some</i> html tags"};
}
}
class MainForm : IForm<HTMLElement,MainForm,MainForm.Outcome> {
public string Title => "Main form";
private readonly MainFormView _view = new MainFormView();
public IFormView<HTMLElement> View => _view;
public event Action<MainForm, Outcome> Ended;
public enum Outcome {
EndRequested,
InfoRequested,
ReplaceMaster
}
public ExternalEventsHandlers ExternalEventsHandlers => ExternalEventsHandlers.Create(
() => Ended?.Invoke(this, Outcome.EndRequested));//makes form cancelable
public MainForm() {
LocalActionBuilder.Build(_view.ShowInfo, () => Ended?.Invoke(this, Outcome.InfoRequested));
LocalActionBuilder.Build(_view.ReplaceMaster, () => Ended?.Invoke(this, Outcome.ReplaceMaster));
}
}
class AltMainFormView : IFormView<HTMLElement> {
public IView<HTMLElement>[] Actions => new IView<HTMLElement>[0];
public RenderElem<HTMLElement>[] Render(HTMLElement parentContainer) {
return new RenderElem<HTMLElement>[] {@"that's an alternative main form.<br>
It has no actions so it's a dead end (press F5 to restart)"};
}
}
class AltMainForm : IForm<HTMLElement,AltMainForm,Unit> {
public string Title => "Main form";
public IFormView<HTMLElement> View { get; } = new AltMainFormView();
public event Action<AltMainForm, Unit> Ended;
public ExternalEventsHandlers ExternalEventsHandlers => ExternalEventsHandlers.Ignore;
}
public static class NavigationFlow {
public static void Run(IFormRenderer<HTMLElement> renderer) {
var mainFrm = new MainForm();
var showInfo = new InformationalMessageForm("Some important info", "Info form title");
var altMainFrm = new AltMainForm();
renderer.ReplaceMaster(mainFrm);
mainFrm.Ended += (form, outcome) => {
switch (outcome) {
case MainForm.Outcome.EndRequested:
renderer.Remove(form);
break;
case MainForm.Outcome.InfoRequested:
renderer.AddPopup(showInfo);
break;
case MainForm.Outcome.ReplaceMaster:
renderer.ReplaceMaster(altMainFrm);
break;
}
};
showInfo.Ended += (form, _) => renderer.Remove(form); //just dismiss this popup (no relevant outcome)
}
}
public class Program {
[Ready]
public static void OnReady() {
Toolkit.InitializeToolkit();
var renderer = Toolkit.DefaultFormRenderer(LayoutModeType.TitleExtra_Actions_Body);
NavigationFlow.Run(renderer);
}
}
}
above program as animation in Chrome:
More sophisticated example that shows how to mix html text with controls and to create actions that are disabled as long as control is not correctly validated.
Also present under open Examples/03SimpleValidation/SimpleValidation.sln
using System;
using Bridge;
using Bridge.Html5;
using Philadelphia.Common;
using Philadelphia.Web;
namespace PutYourProjectNameHere.Client {
class SomeFormView : IFormView<HTMLElement> {
public InputTypeButtonActionView Confirm = new InputTypeButtonActionView("OK").With(x => x.MarkAsFormsDefaultButton());
public IView<HTMLElement>[] Actions => new []{Confirm};
public InputView Inp = new InputView("Some entry");
public RenderElem<HTMLElement>[] Render(HTMLElement parent) {
//notice: you can mix text and controls safely thanks to implicit conversion operators
return new RenderElem<HTMLElement>[] {
"<div style='font-size: 12px'>",
Inp,
"</div>"
};
}
}
class SomeForm : IForm<HTMLElement,SomeForm,CompletedOrCanceled> {
public string Title => "Example form";
private readonly SomeFormView _view = new SomeFormView();
public IFormView<HTMLElement> View => _view;
public event Action<SomeForm, CompletedOrCanceled> Ended;
public ExternalEventsHandlers ExternalEventsHandlers =>
ExternalEventsHandlers.Create(() => Ended?.Invoke(this, CompletedOrCanceled.Canceled));
//comment out above declaration and uncomment next line to make form noncloseable
//public ExternalEventsHandlers ExternalEventsHandlers => ExternalEventsHandlers.Ignore;
public SomeForm() {
var inp = LocalValueFieldBuilder.Build(_view.Inp,
(v, errors) => errors.IfTrueAdd(string.IsNullOrWhiteSpace(v),
"Must contain at least one non whitespace character"));
var conf = LocalActionBuilder.Build(_view.Confirm,
() => Ended?.Invoke(this, CompletedOrCanceled.Completed));
conf.BindEnableAndInitializeAsObserving(x => x.Observes(inp));
}
}
public class Program {
[Ready]
public static void OnReady() {
Toolkit.InitializeToolkit();
var renderer = Toolkit.DefaultFormRenderer();
var msg = new SomeForm();
msg.Ended += (x, outcome) => renderer.Remove(x);
//outcome? for this form it's either Completed or Cancelled. For simplicity we don't do anything with it
renderer.AddPopup(msg);
//comment out former line and uncomment next line to achieve frameless
//renderer.ReplaceMaster(msg);
}
}
}
above yields following outcome in Firefox
In frameless mode it looks this way:
Notice how form title is placed differently without you doing anything. Same for exit action. If you change line
public ExternalEventsHandlers ExternalEventsHandlers => ExternalEventsHandlers.Create(() => Ended?.Invoke(this,CompletedOrCanceled.Canceled));
to
public ExternalEventsHandlers ExternalEventsHandlers => ExternalEventsHandlers.Ignore;
... then those different dismiss form actions will disappear ('X' in dialog or 'door exit' in frameless).
TODO prepare example based on demo code
Create project using template as instructed in Installation section above. As a result you will have simplest server calling example:
- asks for your name
- sends your name to server
- server replies with concatenated name of yours and your IP address
- that reply is displayed in dialog
NOTE: if you change or add anything in interfaces decorated with [HttpService] attribute you need to run dotnet script Services.csx
as otherwise your project won't compile.
Also present under open Examples/04CustomIView/CustomIView.sln
using System;
using System.Collections.Generic;
using System.Linq;
using Bridge;
using Bridge.Html5;
using Philadelphia.Common;
using Philadelphia.Web;
namespace PutYourProjectNameHere.Client {
//first lets define some nonobvious custom class that will be presented by IView
public class Article {
public bool IsBreakingNews {get; set;}
public string Title {get; set;}
public string Author {get; set;}
public DateTime PublishedAt {get; set;}
public string Body {get; set;}
}
//this is a custom IView that is used to render Article
public class ArticleReadOnlyValueView : IReadOnlyValueView<HTMLElement,Article> {
private readonly HTMLDivElement _widget
= new HTMLDivElement {ClassName = typeof(ArticleReadOnlyValueView).Name };
private Article _lastValue;
public event UiErrorsUpdated ErrorsChanged;
public ISet<string> Errors => DefaultInputLogic.GetErrors(_widget);
public HTMLElement Widget => _widget;
public Article Value {
get => _lastValue;
set {
_lastValue = value;
_widget.RemoveAllChildren();
if (value == null) {
return; //normally you would never need this...
}
_widget.AddOrRemoveClass(value.IsBreakingNews, "isBreakingNews");
_widget.AppendChild(new HTMLDivElement {
TextContent = $"{(value.IsBreakingNews ? "Breaking news: " : "")}{value.Title}" });
_widget.AppendChild(new HTMLDivElement {
TextContent = $@"by {value.Author} published at
{I18n.Localize(value.PublishedAt, DateTimeFormat.YMDhm)}" });
_widget.AppendChild(new HTMLDivElement {TextContent = value.Body});
}
}
public void SetErrors(ISet<string> errors, bool causedByUser) {
_widget.SetAttribute(Magics.AttrDataErrorsTooltip, string.Join("\n", errors));
_widget.Style.BackgroundColor = errors.Count <= 0 ? "" : "#ff0000";
ErrorsChanged?.Invoke(this, errors);
}
//FormView->Render() is short thanks to this
public static implicit operator RenderElem<HTMLElement>(ArticleReadOnlyValueView inp) {
return RenderElem<HTMLElement>.Create(inp);
}
}
//due to custom control following FormView is merely instantiating controls (and has almost no logic on its own)
public class NewsReaderFormView : IFormView<HTMLElement> {
public ArticleReadOnlyValueView NewsItem {get; } = new ArticleReadOnlyValueView();
public InputTypeButtonActionView NextItem {get;}
= InputTypeButtonActionView.CreateFontAwesomeIconedButtonLabelless(Magics.FontAwesomeReloadData)
.With(x => x.Widget.Title = "Fetch next newest news item");
public IView<HTMLElement>[] Actions => ActionsBuilder.For(NextItem);
public RenderElem<HTMLElement>[] Render(HTMLElement parentContainer) {
return new RenderElem<HTMLElement>[] {NewsItem};
}
}
//news reader form that informs outside world that user requests next news item or has enough news for today
public class NewsReaderForm : IForm<HTMLElement,NewsReaderForm,NewsReaderForm.ReaderOutcome> {
public enum ReaderOutcome {
FetchNext,
Cancelled
}
public string Title => "News reader";
private readonly NewsReaderFormView _view = new NewsReaderFormView();
public IFormView<HTMLElement> View => _view;
public event Action<NewsReaderForm,ReaderOutcome> Ended;
public ExternalEventsHandlers ExternalEventsHandlers => ExternalEventsHandlers.Create(
() => Ended?.Invoke(this, ReaderOutcome.Cancelled));
public NewsReaderForm() {
LocalActionBuilder.Build(_view.NextItem, () => Ended?.Invoke(this, ReaderOutcome.FetchNext));
}
public void Init(Article itm) {
_view.NewsItem.Value = itm;
}
}
//helper extensions
public static class ArrayExtensions {
public static T RandomItem<T>(this T[] self) {
var i = DateTime.Now.Second % self.Length;
return self[i];
}
public static IEnumerable<T> PickSomeRandomItems<T>(this T[] self, int cnt) {
var x = DateTime.Now.Second % self.Length;
return Enumerable.Range(0, cnt).Select(i => self[(x+i) % self.Length]);
}
}
//entry point
public class Program {
[Ready]
public static void OnReady() {
var di = new DiContainer();
Services.Register(di); //registers discovered services from model
Toolkit.InitializeToolkit();
var renderer = Toolkit.DefaultFormRenderer();
var reader = new NewsReaderForm();
reader.Init(GenerateNewsItem());
reader.Ended += (x, outcome) => {
switch (outcome) {
case NewsReaderForm.ReaderOutcome.Cancelled:
renderer.Remove(x);
break;
case NewsReaderForm.ReaderOutcome.FetchNext:
x.Init(GenerateNewsItem());
break;
}
};
renderer.AddPopup(reader);
}
private static string[] _fnames = {"Anna", "John","Mike", "Paul", "Frank", "Mary", "Sue" };
private static string[] _lnames = {"Doe", "Smith", "Tomatoe", "Potatoe", "Eggplant" };
private static string[] _title1 = {"Cow", "Famous actor","Famous actress", "UFO", "Popular social platform", "Alien", "Cat", "President", "Dog", "Big company CEO", "Law abiding citizen", "Old man", "Young lady" };
private static string[] _title2 = {"ate", "was run over by", "was eaten by", "was surprised by", "stumbled upon" };
private static string[] _title3 = {"decent folks", "whole nation", "neighbour", "Internet", "MEP", "galaxy", "homework", "its fan", "conspiracy theorist crowd" };
private static string[] _body = {"It was completely unexpected.", "It came as a shock to everybody in country",
"Whole nation is in shock.", "President calls for special powers to address unexpected situation.",
"Stock exchange is in panic.", "Majority of MDs call it pandemia.", "Will we ever be able to cope with such tragedy?",
"Major social platforms provides special tools to help its users deal with a tragedy."};
private static Article GenerateNewsItem() {
return new Article {
IsBreakingNews = DateTime.Now.Second % 2 == 0,
PublishedAt = DateTime.Now,
Author = $"{_fnames.RandomItem()} {_lnames.RandomItem()}",
Title = $"{_title1.RandomItem()} {_title2.RandomItem()} {_title3.RandomItem()}",
Body = string.Join("", _body.PickSomeRandomItems(4))
};
}
}
}
additionally paste following code into the header of index-*.html
file(s).
<style type="text/css">
.ArticleReadOnlyValueView {
display: flex;
flex-direction: column;
width: 50vw;
}
.ArticleReadOnlyValueView > div:nth-child(1) {
font-weight: bold;
font-size: larger;
}
.ArticleReadOnlyValueView.isBreakingNews > div:nth-child(1) {
color: red;
}
.ArticleReadOnlyValueView > div:nth-child(2) {
color: gray;
}
.ArticleReadOnlyValueView > div:nth-child(3) {
flex-grow: 1;
}
</style>
This will generate following outcome:
Once you start building richer programs you will likely notice that providing f.e. service implementations to IFlow
s is tedious. DiContainer
is a solution to this problem.
Instead of writing this way:
namespace DependencyInjection.Domain {
public class SomeForm : IForm<HTMLElement,SomeForm,Unit> {
public SomeForm(IHelloWorldService someService) {
//here would be some construction body...
}
//rest of IForm implementation would be present here...
}
public class SomeFlow : IFlow<HTMLElement> {
private readonly SomeForm _someForm;
public SomeFlow(IHelloWorldService helloService, SomeForm someForm) {
_someForm = someForm;
//here would be rest of constructor code...
}
public void Run(IFormRenderer<HTMLElement> renderer, Action atExit) {
//here would be inter-form navigational logic...
}
}
public class ProgramWithoutRichDi {
[Ready]
public static void OnReady() {
var di = new DiContainer();
Services.Register(di); //registers discovered services from model
Toolkit.InitializeToolkit();
var renderer = Toolkit.DefaultFormRenderer();
var helloService = di.Resolve<IHelloWorldService>();
//REMEMBER NEXT LINES...
new SomeFlow(
helloService,
new SomeForm(helloService)
).Run(renderer); //here you are providing whole dependency tree manually
}
}
}
write this instead:
namespace DependencyInjection.Domain {
public class SomeForm : IForm<HTMLElement,SomeForm,Unit> {
public SomeForm(IHelloWorldService someService) {
//here would be some construction body...
}
//rest of IForm implementation would be present here...
}
public class SomeFlow : IFlow<HTMLElement> {
private readonly SomeForm _someForm;
public SomeFlow(IHelloWorldService helloService, SomeForm someForm) {
_someForm = someForm;
//here would be rest of constructor code...
}
public void Run(IFormRenderer<HTMLElement> renderer, Action atExit) {
//here would be inter-form navigational logic...
}
}
public class ProgramWithoutRichDi {
[Ready]
public static void OnReady() {
var di = new DiContainer();
Services.Register(di); //registers discovered services from model
di.Register<SomeFlow>(LifeStyle.Transient);
di.Register<SomeForm>(LifeStyle.Transient);
Toolkit.InitializeToolkit();
var renderer = Toolkit.DefaultFormRenderer();
//...COMPARE WITH FOLLOWING FILE
di.Resolve<SomeFlow>().Run(renderer); //DI container builds SomeFlow instance itself
}
}
}
This example is present under Examples/05DependencyInjection/DependencyInjection.sln
What are the advantages? Your code is generally shorter the more you leverage this pattern. Then you don't need to change all new SomeClass
invocations every time you change constructor signature -> it gives you more flexibility and saves you tedious changes.
NOTE: you can use same dependency injection API on server and on client. On the server implementation delegates to ASP.NET core DI. On the client it uses toolkit provided implemnentation that internally relies on Reflection API. One thing missing on the client currently is LifeTime.Scoped
support (Transient
and Singleton
work fine).
Also notice that API lets you register several implementations for the same key. When you call Resolve<T>()
you get the first registered implementation. If you call ResolveAll<T>()
you will get all instances.
To see way more demos f.e. for grid clone this repo and open Philadelphia.Toolkit.And.Demo.sln. This is source of live demo mentioned in the beginning.
Assuming that you've used template to generate project named 'ExampleApp' you will get following solution:
Everything is covered by Apache 2.0 with the exception of third party font and icons resources. In other words: all source code and CSS scripts are licensed under Apache 2.0. Specifically following files are third party and licensed otherwise:
-
fontawesome-webfont.woff
Font Awesome by Dave Gandy Font License Font Awesome Free is free, open source, and GPL friendly. You can use it for commercial projects, open source projects, or really almost whatever you want. Full Font Awesome Free license.
# Fonts: SIL OFL 1.1 License (https://scripts.sil.org/OFL) In the Font Awesome Free download, the SIL OLF license applies to all icons packaged as web and desktop font files. -
Success.png, Error.png
Part of Koloria free icons set. Used under following terms of use: –Koloria Icon Set can be used in open source or commercial projects for free. –When you mention Koloria Icon Set, please refer to this page/download link. –Do not sell the package on graphic stock sites – this pack is intended to be free. You may include the icons in your commercial themes/designs/creations. – Do not claim to be the author of this icon set. -
Exit.png
Icon 'Enter' made by Icomoon in interface Pack 'Icomoon Free 1' from www.flaticon.com -
drag64.png, drag64rotated.png
Origin http://ionicons.com The premium icon font for Ionic. Designed by benjsperry Terms of use: Note: All brand icons are trademarks of their respective owners. The use of these trademarks does not indicate endorsement of the trademark holder by Drifty, nor vice versa. Visit ionicons.com and check out the search feature, which has keywords identifying common icon names and styles. For example, if you search for “arrow” we call up every icon that could possibly be used as an arrow. We’ve also included each icon’s class name for easy copy/pasting when you’re developing! We intend for this icon pack to be used with Ionic, but it’s by no means limited to it. Use them wherever you see fit, personal or commercial. They are free to use and licensed under MIT -
spin32.gif spin84.gif
Taken from http://loading.io Terms of use: All materials used in generating animated icons in this website, except the g0v icon, are created by loading.io. You can use them freely for any purpose. -
Search.png
Taken from https://www.visualpharm.com/must_have_icon_set/ Must Have Icons License: The icons are free for personal use and also free for commercial use, but we require linking to our web site. -
Xlsx.png
Taken from https://www.iconfinder.com/iconsets/brands-applications Excel Icon By Iconfinder ApS License: Creative Commons (Unported 3.0)
Images and pdf files in Philadelphia.Demo.Client\ImagesForUploadDemo\Full
and Philadelphia.Demo.Client\ImagesForUploadDemo\Thumb
that are used for demo of UploadView. Dominik Pytlewski is its sole author. All those files are in public domain - Dominik doesn't claim any copyright on those files.
You are welcome to contribute! If you find a bug report it via issue please. If you know how to fix it then please provide a fix by:
- forking repo
- creating local branch from master
- preparing fix within that local branch
- submitting pull request referencing that issue
If you would like to implement new feature please create an issue first so that we can discuss it.
- On the server
it is automatically provided by toolkit and configured during server start up - you decorate a service interface with a[HttpService]
attribute and server automatically registers implementation creating all necessary code. - On the client
it currently relies on code generation utilizing dotnet script. If you change service contract and forget to run it then project won't compile saving you from runtime errors. Technically speaking you can use client side library on its own (without server side libraries) to leverage its widgets library. In that case only thing you miss would be that seamless integration with services described above.
- Declarative forms
as shown in validation example above - you specify form title, its content and it's actions (buttons). Afterwards you pass it to renderer that can display it as modal dialog or as frameless dialog (simply: as children of given DOM element). Renderer handles its proper display and ESC and ENTER keys handling. Frameless forms have configurable layout (seeLayoutModeType
) so that you can specify where title, form body and actions are placed visually (internally it utilizesdisplay: grid
for this purpose) - Dropdowns
- multicolumn single choice with filter and clear action
SingleChoiceDropDown<T>
- multiple choice dropdowns that doesn't rely on user's knowledge of CTRL+click combination
MultipleChoiceDropDown<T>
- multicolumn single choice with filter and clear action
- Datagrid
supporting per column filtering, sorting and grouping+aggregations, search in grid, export to spreadsheet - Date and DateTime pickers
DateTimePickerView
for single value;DateTimeRangeView
for ranges UploadView
upload and download pickers for files with two modes: file items as rows-in-a-table or thumbnails-in-a-grid- Declarative tooltips
DOM 'listens' for changes to attributesdata-errors-tooltip='Some tooltip content'
ordata-disabled-tooltip="Another tooltip content"
internaly utilizing DOM mutation observers and WeakMap - Horizontal and Vertical panels
resizeable and hideable
Tookit uses Model-View-Presenter UI architectural pattern to achieve separation of concerns. Business logic resides in rich model. View should only be concerned with visual needs. Generally there's an assumption that you have variables in code that are:
- observable
- validable
- provide interface to change them (mutate them)
Upon change they validate themselves and raise events so that others may act. That's your Model.
On the other hand there are widgets (DOM elements wrapped so that they expose similar observable interfaces as Model). They expose events so that one can be notified that view was changed (by user or programmatically). Now toolkit has means to bilaterally connect model with view so that:
- changes to Model variables are shown in View
- changes in View are (indirectly) validated in Model. On success change is accepted. On failure errors are replicated to View (and typically shown in tooltip)
- Model variables can interact and let us compute 'is form valid logic' hence we can use it for other means such as disabling actions
In other words we have rich databinding:
- read only binding: create your observable model variable defining its type and set of validators. Ask toolkit to propagate its current value to UI widget
- readwrite binding: same as above but also update model when UI changes
- enablement binding: actions (buttons etc) are visually "clickable" only when model permits
- that model may be an aggregation of post validation status from set of widgets
- Model variable
something that holds value, that can be given rules to validate that value, it has means to try to mutate that value as a result it will advertise if it was successfully changed or rejected change due to some reason.
public interface IReadOnlyValue<ValueT> {
ValueT Value { get; }
IEnumerable<string> Errors { get; }
event ValueChangedRich<ValueT> Changed;
}
public delegate void Validate<T>(T newValue, ISet<string> errors);
public interface IReadWriteValue<ValueT> : IReadOnlyValue<ValueT> {
event Validate<ValueT> Validate;
void Reset(bool isUserChange = false, object sender = null);
Task<Unit> DoChange(ValueT newValue, bool isUserChange, object sender=null, bool mayBeRejectedByValidation=true); //due to user input or programmatic
}
- Widget
something that can be added to DOM, that informs when it was changed
public interface IView<WidgetT> {
WidgetT Widget { get; }
}
public delegate void UiErrorsUpdated(object sender, ISet<string> errors);
public interface IReadOnlyValueView<WidgetT,ValueT> : IView<WidgetT> {
event UiErrorsUpdated ErrorsChanged;
/// <summary>doesn't cause IReadWriteValueView->Changed to be raised</summary>
ValueT Value { get; set; }
ISet<string> Errors { get; }
void SetErrors(ISet<string> errors, bool causedByUser); //includes validation,conversion and remote save errors
}
- where is Model and View in former 'simple validation' example?
View variable is declared in line:
public InputView Inp = new InputView("Some entry");
In this line is a 'combo' that creates: Model variable, binds it to View variable _view.Inp
and attaches validator
var inp = LocalValueFieldBuilder.Build(_view.Inp,
(v, errors) => errors.IfTrueAdd(string.IsNullOrWhiteSpace(v),
"Must contain at least one non whitespace character"));
And here's another combo - this time for 'action'. It creates: Model for View that is clickable only when inp is properly validated
var conf = LocalActionBuilder.Build(_view.Confirm,
() => Ended?.Invoke(this, CompletedOrCanceled.Completed));
conf.BindEnableAndInitializeAsObserving(x => x.Observes(inp));
The only abstractions left that you should use for succinct code are:
Philadelphia.Common.IForm
that defines collection of Model variables that may request to 'end'Philadelphia.Common.IFormView
that defines collection of View variables used by IForm to define how form exactly looksPhiladelphia.Common.IFormRenderer
that is abstraction over HTML's element appendChild() method (for it isToolkit.DefaultFormRenderer()
). That interface allows you to show&hide instances of IForm.
Server side needs to be given references to assemblies that contain service interfaces and its implementations. During server start it looks for classes implementing IDiInstaller
interfaces to instance them and invoke their sole Install
method. It serves same purpose as in Windsor installer.
On client side simple DiContainer is also available.
Toolkit supports several scenarios on the server:
- regular service methods that accept JSONized input and spit out JSONized output
- that may optionally accept uploaded files
- that may optionally produce inline or attachment files
- Server-sent event listener
Additionally on the server you can use following filter to do some AOP tasks such as logging requests, blocking requests, authentication, db transactions, etc.
interface Philadelphia.Server.Common.ILifetimeFilter {
Task OnServerStarted(IDiResolveReleaseOnlyContainer di);
//depending on result connection will continue or be rejected
Task<ConnectionAction> OnConnectionBeforeHandler(IDiResolveReleaseOnlyContainer di, string url, object serviceInstance, MethodInfo m, ResourceType resource);
}
///not invoked for static resources
///not invoked when OnConnectionBeforeHandler returned filter.
///null Exception means success.
Task OnConnectionAfterHandler(object connectionCtx, IDiResolveReleaseOnlyContainer di, Exception exOrNull);
More specifically
Decorate your service interfaces with single marker attribute [HttpService] and toolkit will understand that you want it to expose methods in that interface:
- when method is of form
Task<T> MethodName(/*parameters*/)
then expose it as POST method - additionally when method is of form
Task<FileModel> MethodName(/*parameters*/)
then expose it as GET method as well - when method is of form
Task<Func<T,bool> MethodName(U input)
then expose it as Server-sent events listener
When you follow those conventions then not only server side part of toolkit discovers your services but also client side code generation builds for you code compatible with:
- UploadView - upload&download widget
see example class in demo project:Philadelphia.Demo.Client.ISomeService_OrderAttachment
- Server-sent events subscription
example class in demo project:Philadelphia.Demo.Client.ISomeService_ContinentalListener_SseSubscriber
- Regular POST methods
example class in demo project:Philadelphia.Demo.Client.WebClientSomeService
What does it do for regular GET&POST method?
- looks for implementation of this interface to
- expose its methods as POST handlers under urls Full.Interface.Namespace.InterfaceName/MethodName that receive parameters as JSON and return outcome as JSON. Specifically:
- if a method has single parameter then it is be assumed to be directly serialized to JSON
- if a method has multiple parameters then it is be assumed to be wrapped into System.Tuple<T1,T2,...> to JSON and successful outcome serialized to JSON
For Server-sent events methods:
- toolkit registers subscription for events of type
T
that HTTP clients can subscribe to. Clients can limit events to scope defined inU
. Example:T
can be a news item andU
may be its category such asscience
. - efficient async handler is automatically created by toolkit based on actor model pattern as provided by F# MailboxProcessor
- in your service implementation only interaction you need is:
- add
Subscription<T,U>
parameter to your service constructor so that you can post messages - return null in
Task<Func<T,bool> MethodName(U input)
if you want to reject particular client OR - return actual function to filter messages for given client f.e.
Func<NewsItem,bool> NewsListener(NewsCategory inp) { return x => x.Category == inp; }
- add
Additionally regular POST methods may also deal with file uploads and downloads. When POST method should return file (as attachment or inline) it should return instance of Philadelphia.Common.FileModel
. It can return files made 'on fly' or otherwise created.
For uploads convention is used that your method needs to have signature:
//method name must end with 'Setter', have following first parameter and following result. You can add other parameters if needed
Task<RemoteFileId[]> SomeFancyNameSetter(UploadInfo info, int myFirstParam, bool myOtherParam);
For downloads convention is:
Task<FileModel> WhateverName(int yourArbitrary, object parametersIfNeeded);
BUT if you create pair of getter and setter
Task<FileModel> SomethingGetter(RemoteFileId fileIdentifier, SomeType someVal /* more params if needed*/);
Task<RemoteFileId[]> SomethingSetter(UploadInfo info, SomeType someVal /* more params if needed*/);
then such pair may be used together with Philadelphia.Web.UploadView
upload&download widget. See demo code on how to use it.
If there is any [HttpService] without implementation - you get crash during startup (we don't like runtime errors if we can detect bugs earlier).
Static files (images, css, compiled js file etc) are declared to be exposed publicly by using static_resources.json file(s) such as this one:
[
{
"Url": "/",
"FileSystemPath": "../PhiladelphiaPowered.Client/index-min.html",
"MimeType": "text/html"
},
{
"Url": "/{0}",
"Dir": "../PhiladelphiaPowered.Client/Bridge/output",
"FilePattern": "(?i)\\.js$",
"MimeType": "application/javascript"
},
{
"Url": "/{0}",
"Dir": "../packages/Philadelphia.StaticResources.*/content",
"FilePattern": "(?i)\\.css$",
"MimeType": "text/css"
},
//etc
]
NOTE: last line contains '*' - toolkit is able to find 'sole matching directory'. If it finds zero or more than one then it will throw userfriendly exception during start NOTE: paths are relative to that json file location. Static resource json file(s) are loaded once at startup of server for better security and performance (sacrificing some memory).
- Several releases ago Bridge.NET created as many
js
files as there were unique namespaces. In current version it is possible to create onejs
per project (this is what we use). That is why all classes per project are in the same namespace (as otherwise we would need to frequently edit index.html file). That is a legacy and to be considered if it is worth changing. If yes it also requires changes to CSS selectors instyles.css
as by convention they are in form.Full\.Namespace\.Of\.Widget\.WidgetClassName {}
- I18n
We don't force on you particular internationalization tool but we ourselves are happy using our other project Oldschool.I18n
This is general info only. We don't guarantee anything. See license for more details
- Reading files from disk as described elsewhere in this document, server side doesn't look for anything on filesystem (disk) during requests processing. It does everything during its startup. It can be thought as a limitation as toolkit doesn't detect new or deleted files (and startup is slightly slower). On the other hand it is way more secure and processes requests faster (RAM performance > SSD disk performance).
- XSS / Cross site scripting
By default toolkit uses DOM Element's TextContent property to populate its content (instead of InnerHTML). This normally protects against HTML and JS injection. Yet if you have safe input (f.e. static HTML) you are able to use it but you do it on your own risk.
More info: look for usage ofPhiladelphia.Web.TextType
- CSRF / Cross site request forgery
HTTP(s) requests build by toolkit by classes:Philadelphia.Web.HttpRequester
andPhiladelphia.Web.XMLHttpRequestImplementingIPromise
add CSRF token as either request header field or as a POST form parameter. Server side part if it finds this token in request it puts it into instance ofPhiladelphia.Common.ClientConnectionInfo
. You can check cookies and CSRF token either in service itself or (preferred) in methodOnConnectionBeforeHandler
ofILifetimeFilter
. How to use it client side? UsePhiladelphia.Web.AuthenticationThenMainMenuFlow
or study it to see how to populate token to be used in requests. Make sure your CORS policy is strict. - BREACH By default compression is disabled for HTTPS. If you protect all nonauthenticated requests to services then normally you should be safe to enable it for HTTPS as well.
Dominik Pytlewski and Tomasz Sztokinier We are two guys behind TODO IT Spółka z o.o. in Warsaw, Poland - limited company.
Originally developed as proof of concept in 2015. It is loosely inspired by GWT project. We were heavy users of that project in 2008-2013. In 2014 Microsoft started changing its countenance by opening up to its users. We abandoned Java as it seemed stagnated and opted for dotnet in Mono hoping for the better long term future. We yearned for integrated, statically typed, code shareable environment between those two. We wanted to utilize more powerful C# language and new browser abilities. At that time toolkit utilized Saltarelle compiler and NancyFx to be able to host it in Mono under Linux. In 2016 it was ported to Bridge.NET. Later that year our first commercial project was started. In 2018 after few successful projects we ported it to dotnet core and aspnet core. We did it to bring better crossplatform support, better performance and to bring SSE support.
In future we expect that once WebAssembly matures together with CoreRT AOT's webassembly compilation target support it should be possible to also target CoreRT+WebAssembly instead of (only) BridgeDotNet+JavaScript.