Skip to content

Commit

Permalink
Binding component (#271)
Browse files Browse the repository at this point in the history
 (marked as experimental)

Adds `StaticComponent`
Adds `IReadabble.ImmediateObservable` and `IReadabble.ImmediateObservableAny`
Adds `IAvaloniaObject.Bind`

Adds `IAvaliniaObject.init` / Init function attribute

---------

Co-authored-by: JaggerJo <ail@jaggerjo.com>
  • Loading branch information
JaggerJo and JaggerJo authored Feb 15, 2023
1 parent dd1db8b commit f428256
Show file tree
Hide file tree
Showing 12 changed files with 290 additions and 11 deletions.
10 changes: 10 additions & 0 deletions src/Avalonia.FuncUI.sln
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_Solution Items", "_Solutio
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Examples.InlineText", "Examples\Component Examples\Examples.InlineText\Examples.InlineText.fsproj", "{B8D8C84B-05AD-475B-BE81-A30544CE0149}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Examples.ViewModelComponent", "Examples.ViewModelComponent", "{24AD0B82-9D10-4929-8FFE-E5E048C11842}"
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Examples.ViewModelComponent.CounterApp", "Examples\ViewModelComponent Examples\Examples.ViewModelComponent.CounterApp\Examples.ViewModelComponent.CounterApp.fsproj", "{1F3A7A69-5D68-4DB1-B1EF-A845ABB255EC}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -133,6 +137,10 @@ Global
{B8D8C84B-05AD-475B-BE81-A30544CE0149}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B8D8C84B-05AD-475B-BE81-A30544CE0149}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B8D8C84B-05AD-475B-BE81-A30544CE0149}.Release|Any CPU.Build.0 = Release|Any CPU
{1F3A7A69-5D68-4DB1-B1EF-A845ABB255EC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1F3A7A69-5D68-4DB1-B1EF-A845ABB255EC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1F3A7A69-5D68-4DB1-B1EF-A845ABB255EC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1F3A7A69-5D68-4DB1-B1EF-A845ABB255EC}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -154,6 +162,8 @@ Global
{BF5DC7CC-7ABF-40AB-97DD-6774C17FE005} = {F50826CE-D9BC-45CF-A110-C42225B75AD3}
{5CC37986-D6E0-438B-B895-BC82DFD22307} = {F50826CE-D9BC-45CF-A110-C42225B75AD3}
{B8D8C84B-05AD-475B-BE81-A30544CE0149} = {F50826CE-D9BC-45CF-A110-C42225B75AD3}
{24AD0B82-9D10-4929-8FFE-E5E048C11842} = {84811DB3-C276-4F0D-B3BA-78B88E2C6EF0}
{1F3A7A69-5D68-4DB1-B1EF-A845ABB255EC} = {24AD0B82-9D10-4929-8FFE-E5E048C11842}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {4630E817-6780-4C98-9379-EA3B45224339}
Expand Down
3 changes: 2 additions & 1 deletion src/Avalonia.FuncUI.sln.DotSettings
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/UserDictionary/Words/=Adorner/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Avalonia/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Avalonia/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=seealso/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
2 changes: 2 additions & 0 deletions src/Avalonia.FuncUI/Avalonia.FuncUI.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
<Compile Include="Components\State\State.fs" />
<Compile Include="Components\State\State.Adapters.fs" />
<Compile Include="Components\State\State.Functions.fs" />
<Compile Include="Components\StaticComponent.fs" />
<Compile Include="Components\Context\Context.EffectsHook.fs" />
<Compile Include="Components\Context\Context.StateHook.fs" />
<Compile Include="Components\Context\Context.fs" />
Expand All @@ -50,6 +51,7 @@
<Compile Include="DataTemplateView.fs" />
<Compile Include="Helpers.fs" />
<Compile Include="View.fs" />
<Compile Include="DSL\Base\AvaloniaObject.fs" />


<Compile Include="DSL\Base\Layoutable.fs" />
Expand Down
10 changes: 9 additions & 1 deletion src/Avalonia.FuncUI/Components/State/State.Functions.fs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
namespace Avalonia.FuncUI

open System
open System.Runtime.CompilerServices
open Avalonia.FuncUI

[<RequireQualifiedAccess>]
Expand Down Expand Up @@ -44,4 +45,11 @@ module State =
let readTryFindByKey (keyPath: 'value -> 'key) (key: IReadable<'key>) (wire: IReadable<list<'value>>) : IReadable<'value option> =
let keyedWire: IReadable<Map<'key, 'value>> = new ReadValueMap<'value, 'key>(wire, keyPath) :> _
let keyFocusedWire: IReadable<'value option> = new ReadKeyFocusedValue<'value, 'key>(keyedWire, key) :> _
keyFocusedWire
keyFocusedWire

[<Extension>]
type __IReadableExtensions =

[<Extension>]
static member Map<'a, 'b> (value: IReadable<'a>, mapFunc: 'a -> 'b) : IReadable<'b> =
State.readMap mapFunc value
32 changes: 32 additions & 0 deletions src/Avalonia.FuncUI/Components/State/State.fs
Original file line number Diff line number Diff line change
Expand Up @@ -125,20 +125,52 @@ module StateExtensions =
currentGeneric.GetMethod.Invoke(this, Array.empty)


/// <summary>
/// Observable for non generic state values. Fires whenever the value changes.
/// <seealso cref="ImmediateObservableAny"/>
/// </summary>
member this.ObservableAny with get () : IObservable<obj> =
{ new IObservable<obj> with
member _.Subscribe(observer: IObserver<obj>) =
this.SubscribeAny(observer.OnNext)
}

/// <summary>
/// Observable for non generic state values. Fires whenever the value changes and immediately after subscribing.
/// <seealso cref="ObservableAny"/>
/// </summary>
[<Experimental "Same as ObservableAny, but fires once immediately after subscribing">]
member this.ImmediateObservableAny with get () : IObservable<obj> =
{ new IObservable<obj> with
member _.Subscribe(observer: IObserver<obj>) =
observer.OnNext this.CurrentAny
this.SubscribeAny(observer.OnNext)
}

type IReadable<'value> with

/// <summary>
/// Observable for state values. Fires whenever the value changes.
/// <seealso cref="ImmediateObservable"/>
/// </summary>
member this.Observable with get () : IObservable<'value> =
{ new IObservable<'value> with
member _.Subscribe(observer: IObserver<'value>) =
this.Subscribe(observer.OnNext)
}

/// <summary>
/// Observable for state values. Fires whenever the value changes and immediately after subscribing.
/// <seealso cref="Observable"/>
/// </summary>
[<Experimental "Same as Observable, but fires once immediately after subscribing">]
member this.ImmediateObservable with get () : IObservable<'value> =
{ new IObservable<'value> with
member _.Subscribe(observer: IObserver<'value>) =
observer.OnNext this.Current
this.Subscribe(observer.OnNext)
}

type IWritable<'value> with

member this.Observer with get () : IObserver<'value> =
Expand Down
25 changes: 25 additions & 0 deletions src/Avalonia.FuncUI/Components/StaticComponent.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
namespace Avalonia.FuncUI

open Avalonia
open Avalonia.Controls
open Avalonia.FuncUI.Types
open Avalonia.Styling

[<AbstractClass>]
[<Experimental "Statically construct views with F#">]
type StaticComponent () =
inherit Border ()

override this.OnInitialized () =
base.OnInitialized ()

this.DataContext <- this
this.Child <-
()
|> this.Build
|> VirtualDom.VirtualDom.create

abstract member Build: unit -> IView

interface IStyleable with
member this.StyleKey = typeof<Border>
40 changes: 40 additions & 0 deletions src/Avalonia.FuncUI/DSL/Base/AvaloniaObject.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
namespace Avalonia.FuncUI.DSL

open Avalonia
open Avalonia.FuncUI
open Avalonia.FuncUI.Types

[<AutoOpen>]
module AvaloniaObject =

type IAvaloniaObject with

/// <summary>
/// Hook into the controls lifetime. This is called when the backing avalonia control is created.
/// <example>
/// <code>
/// TextBlock.create [
/// TextBlock.init (fun textBlock -&gt;
/// textBlock.Bind(TextBlock.TextProperty, counter.Map string)
/// textBlock.Bind(TextBlock.ForegroundProperty, counter.Map (fun c -&gt;
/// if c &lt; 0
/// then Brushes.Red :&gt; IBrush
/// else Brushes.Green :&gt; IBrush
/// ))
/// )
/// ]
/// </code>
/// </example>
/// </summary>
static member init<'t when 't :> AvaloniaObject>(func: 't -> unit) : IAttr<'t> =
Attr.InitFunction {
InitFunction.Function = (fun (control: obj) -> func (control :?> 't))
}

member this.Bind(prop: DirectPropertyBase<'value>, readable: #IReadable<'value>) : unit =
let _ = this.Bind(property = prop, source = readable.ImmediateObservable)
()

member this.Bind(prop: StyledPropertyBase<'value>, readable: #IReadable<'value>) : unit =
let _ = this.Bind(property = prop, source = readable.ImmediateObservable)
()
27 changes: 24 additions & 3 deletions src/Avalonia.FuncUI/Types.fs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ open Avalonia
open Avalonia.Controls
open System
open System.Threading
open Avalonia.Data

module Types =

Expand Down Expand Up @@ -76,12 +77,20 @@ module Types =

override this.GetHashCode () =
(this.Name, this.FuncType, this.Scope).GetHashCode()


[<Struct; CustomEquality; NoComparison>]
type InitFunction =
{ Function: obj -> unit }

override this.Equals (obj: obj) =
false

type IAttr =
abstract member UniqueName : string
abstract member Property : Property option
abstract member Content : Content option
abstract member Subscription : Subscription option
abstract member InitFunction: InitFunction option

type IAttr<'viewType> =
inherit IAttr
Expand All @@ -90,6 +99,7 @@ module Types =
| Property of Property
| Content of Content
| Subscription of Subscription
| InitFunction of InitFunction

interface IAttr<'viewType>

Expand All @@ -106,7 +116,11 @@ module Types =
| Accessor.AvaloniaProperty p -> p.Name
| Accessor.InstanceProperty p -> p.Name

| Subscription subscription -> subscription.Name
| Subscription subscription ->
subscription.Name

| InitFunction _ ->
"initFunction"

member this.Property =
match this with
Expand All @@ -123,6 +137,11 @@ module Types =
| Subscription value -> Some value
| _ -> None

member this.InitFunction =
match this with
| InitFunction value -> Some value
| _ -> None

type IView =
abstract member ViewType: Type with get
abstract member ViewKey: string voption
Expand Down Expand Up @@ -163,4 +182,6 @@ module Types =

let internal (|Subscription'|_|) (attr: IAttr) =
attr.Subscription


let internal (|InitFunction|_|) (attr: IAttr) =
attr.InitFunction
21 changes: 18 additions & 3 deletions src/Avalonia.FuncUI/VirtualDom/VirtualDom.Delta.fs
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,31 @@ open Avalonia.FuncUI.Types

module internal rec Delta =

[<CustomEquality; NoComparison>]
type AttrDelta =
| Property of PropertyDelta
| Content of ContentDelta
| Subscription of SubscriptionDelta
| SetupFunction of InitFunction

override this.Equals (other: obj) : bool =
match other with
| :? AttrDelta as other ->
match this, other with
| Property a, Property b -> a.Equals b
| Content a, Content b -> a.Equals b
| Subscription a, Subscription b -> a.Equals b
| SetupFunction a, SetupFunction b -> a.Equals b
| _ -> false
| _ ->
false

static member From (attr: IAttr) : AttrDelta =
match attr with
| Property' property -> Property (PropertyDelta.From property)
| Content' content -> Content (ContentDelta.From content)
| Subscription' subscription -> Subscription (SubscriptionDelta.From subscription)
| InitFunction bindingSetup -> SetupFunction bindingSetup
| _ -> raise (Exception "unknown IAttr type. (not a Property, Content ore Subscription attribute)")


Expand Down Expand Up @@ -73,7 +88,7 @@ module internal rec Delta =
static member From (content: Content) : ContentDelta =
{ Accessor = content.Accessor;
Content = ViewContentDelta.From content.Content }

type ViewContentDelta =
| Single of ViewDelta option
| Multiple of ViewDelta list
Expand Down Expand Up @@ -109,7 +124,7 @@ module internal rec Delta =
ConstructorArgs = view.ConstructorArgs
KeyDidChange = defaultArg keyDidChange false
Outlet = view.Outlet}

override this.Equals(other) =
match other with
| :? ViewDelta as other ->
Expand All @@ -119,6 +134,6 @@ module internal rec Delta =
this.KeyDidChange = other.KeyDidChange &&
(ValueOption.isSome this.Outlet = ValueOption.isSome other.Outlet)
| _ -> false

override this.GetHashCode() =
HashCode.Combine(this.ViewType, this.Attrs, this.ConstructorArgs, this.KeyDidChange, ValueOption.isSome this.Outlet)
15 changes: 12 additions & 3 deletions src/Avalonia.FuncUI/VirtualDom/VirtualDom.Patcher.fs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
namespace Avalonia.FuncUI.VirtualDom

open Avalonia.FuncUI.VirtualDom.Delta

module internal rec Patcher =
open System
open System.Collections
Expand Down Expand Up @@ -207,15 +209,15 @@ module internal rec Patcher =
patchContentSingle view attr.Accessor single
| ViewContentDelta.Multiple multiple ->
patchContentMultiple view attr.Accessor multiple

let patch (view: IAvaloniaObject, viewElement: ViewDelta) : unit =
for attr in viewElement.Attrs do
match attr with
| AttrDelta.Property property -> patchProperty view property
| AttrDelta.Content content -> patchContent view content
| AttrDelta.Subscription subscription ->
match view with
| :? IControl as control ->
| :? IControl as control ->
patchSubscription control subscription
| _ -> failwith "Only controls can have subscriptions"

Expand All @@ -235,5 +237,12 @@ module internal rec Patcher =
| ValueNone -> ()

control.SetValue(ViewMetaData.ViewIdProperty, Guid.NewGuid()) |> ignore
Patcher.patch (control, viewElement)

for attr in viewElement.Attrs do
match attr with
| AttrDelta.Content content -> Patcher.patchContent control content
| AttrDelta.Subscription s -> Patcher.patchSubscription (control :?> IControl) s
| AttrDelta.Property property -> Patcher.patchProperty control property
| AttrDelta.SetupFunction setupFunction -> setupFunction.Function(control)

control
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
</PropertyGroup>

<ItemGroup>
<Compile Include="Program.fs"/>
</ItemGroup>

<ItemGroup>
<PackageReference Include="Avalonia.Desktop" Version="$(AvaloniaVersion)" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="$(AvaloniaVersion)" />
<ProjectReference Include="..\..\..\Avalonia.FuncUI\Avalonia.FuncUI.fsproj" />
</ItemGroup>

</Project>
Loading

0 comments on commit f428256

Please sign in to comment.