diff --git a/src/ReactiveElmish.Avalonia/ReactiveElmish.Avalonia.fsproj b/src/ReactiveElmish.Avalonia/ReactiveElmish.Avalonia.fsproj
index 88ef519..aa404fd 100644
--- a/src/ReactiveElmish.Avalonia/ReactiveElmish.Avalonia.fsproj
+++ b/src/ReactiveElmish.Avalonia/ReactiveElmish.Avalonia.fsproj
@@ -17,7 +17,7 @@
MIT
https://github.com/JordanMarr/ReactiveElmish.Avalonia
Avalonia F# fsharp Elmish Elm
- 1.1.1
+ 1.1.2
$(OtherFlags) --warnon:1182
$(TargetsForTfmSpecificBuildOutput);CopyProjectReferencesToPackage
diff --git a/src/ReactiveElmish.sln b/src/ReactiveElmish.sln
index 4b0a937..1f5e969 100644
--- a/src/ReactiveElmish.sln
+++ b/src/ReactiveElmish.sln
@@ -19,6 +19,8 @@ Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "ReactiveElmish", "ReactiveE
EndProject
Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "ReactiveElmish.Wpf", "ReactiveElmish.Wpf\ReactiveElmish.Wpf.fsproj", "{8E7D1F76-A45C-4151-96BD-BFEAECAA4419}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WpfExample", "WpfExample\WpfExample.csproj", "{E612993E-238C-40C7-A39C-DA021DBF6D9F}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -41,12 +43,17 @@ Global
{8E7D1F76-A45C-4151-96BD-BFEAECAA4419}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8E7D1F76-A45C-4151-96BD-BFEAECAA4419}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8E7D1F76-A45C-4151-96BD-BFEAECAA4419}.Release|Any CPU.Build.0 = Release|Any CPU
+ {E612993E-238C-40C7-A39C-DA021DBF6D9F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E612993E-238C-40C7-A39C-DA021DBF6D9F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E612993E-238C-40C7-A39C-DA021DBF6D9F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E612993E-238C-40C7-A39C-DA021DBF6D9F}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{B5F6F996-01DD-410D-9D3B-A1E91A3D5CF4} = {B1F3D2D8-D07C-4115-ACAD-5CD90ED1A04A}
+ {E612993E-238C-40C7-A39C-DA021DBF6D9F} = {B1F3D2D8-D07C-4115-ACAD-5CD90ED1A04A}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {E4A43E5C-8FF0-434B-8C5E-AB656A275C4F}
diff --git a/src/ReactiveElmish/ReactiveBindingsCS.fs b/src/ReactiveElmish/ReactiveBindingsCS.fs
new file mode 100644
index 0000000..0a044b3
--- /dev/null
+++ b/src/ReactiveElmish/ReactiveBindingsCS.fs
@@ -0,0 +1,87 @@
+namespace ReactiveElmish
+
+open System
+open System.Runtime.CompilerServices
+open System.Runtime.InteropServices
+open DynamicData
+
+/// Allows using an IStore<'Model> + ReactiveElmishViewModel binding methods from an existing view model using C# Action and Func delegates.
+type ReactiveBindingsCS(onPropertyChanged: Action) =
+ let vm = new ReactiveElmishViewModel(onPropertyChanged.Invoke)
+ let opt = Option.ofObj
+
+ member this.Bind<'Model, 'ModelProjection>(
+ store: IStore<'Model>,
+ modelProjection: Func<'Model, 'ModelProjection>,
+ [] vmPropertyName
+ ) = vm.Bind(store, modelProjection.Invoke, vmPropertyName)
+
+ member this.BindOnChanged<'Model, 'OnChanged, 'ModelProjection>(
+ store: IStore<'Model>,
+ onChanged: Func<'Model, 'OnChanged>,
+ modelProjection: Func<'Model, 'ModelProjection>,
+ [] vmPropertyName
+ ) = vm.BindOnChanged(store, onChanged.Invoke, modelProjection.Invoke, vmPropertyName)
+
+ member this.BindList<'Model, 'ModelProjection>(
+ store: IStore<'Model>,
+ modelProjectionSeq: Func<'Model, 'ModelProjection seq>,
+ [] vmPropertyName
+ ) = vm.BindList(store, modelProjectionSeq.Invoke, vmPropertyName)
+
+ member this.BindList<'Model, 'ModelProjection, 'Mapped>(
+ store: IStore<'Model>,
+ modelProjectionSeq: Func<'Model, 'ModelProjection seq>,
+ map: Func<'ModelProjection, 'Mapped>,
+ [] vmPropertyName
+ ) = vm.BindList(store, modelProjectionSeq.Invoke, map.Invoke, vmPropertyName)
+
+ member this.BindKeyedList<'Model, 'Key, 'Value, 'Mapped when 'Value : equality and 'Mapped : not struct and 'Key : comparison>(
+ store: IStore<'Model>,
+ modelProjection: Func<'Model, Map<'Key, 'Value>>,
+ map: Func<'Value, 'Mapped>,
+ getKey: Func<'Mapped, 'Key>,
+ [] update: Action<'Value, 'Mapped>,
+ [] sortBy: Func<'Mapped, IComparable>,
+ [] vmPropertyName
+ ) = vm.BindKeyedList(store, modelProjection.Invoke, map.Invoke, getKey.Invoke, ?update = opt update, ?sortBy = opt sortBy, vmPropertyName = vmPropertyName)
+
+ member this.BindKeyedList<'Model, 'Key, 'Value when 'Value: equality and 'Value : not struct and 'Key : comparison>(
+ store: IStore<'Model>,
+ modelProjection: Func<'Model, Map<'Key, 'Value>>,
+ getKey: Func<'Value, 'Key>,
+ [] update: Action<'Value, 'Value>,
+ [] sortBy: Func<'Value, IComparable>,
+ [] vmPropertyName: string
+ ) = vm.BindKeyedList(store, modelProjection.Invoke, getKey.Invoke, ?update = opt update, ?sortBy = opt sortBy, vmPropertyName = vmPropertyName)
+
+ member this.BindSourceList<'T>(
+ sourceList: ISourceList<'T>,
+ [] vmPropertyName
+ ) = vm.BindSourceList(sourceList, vmPropertyName)
+
+ member this.BindSourceList<'T, 'Mapped>(
+ sourceList: ISourceList<'T>,
+ map: Func<'T, 'Mapped>,
+ [] vmPropertyName
+ ) = vm.BindSourceList(sourceList, map.Invoke, vmPropertyName)
+
+ member this.BindSourceCache<'Value, 'Key>(
+ sourceCache: IObservableCache<'Value, 'Key>,
+ [] sortBy,
+ [] vmPropertyName
+ ) = vm.BindSourceCache(sourceCache, ?sortBy = opt sortBy, vmPropertyName = vmPropertyName)
+
+ member this.BindSourceCache<'Value, 'Key, 'Mapped when 'Value : not struct and 'Mapped : not struct>(
+ sourceCache: IObservableCache<'Value, 'Key>,
+ map: Func<'Value, 'Mapped>,
+ [] update: Action<'Value, 'Mapped>,
+ [] sortBy,
+ [] vmPropertyName
+ ) = vm.BindSourceCache(sourceCache, map.Invoke, ?update = opt update, ?sortBy = opt sortBy, vmPropertyName = vmPropertyName)
+
+ member this.Subscribe(observable, handler) =
+ vm.Subscribe(observable, handler)
+
+ interface IDisposable with
+ member this.Dispose() = (vm :> IDisposable).Dispose()
diff --git a/src/ReactiveElmish/ReactiveElmish.fsproj b/src/ReactiveElmish/ReactiveElmish.fsproj
index 1239ac5..a1a1d26 100644
--- a/src/ReactiveElmish/ReactiveElmish.fsproj
+++ b/src/ReactiveElmish/ReactiveElmish.fsproj
@@ -17,7 +17,7 @@
MIT
https://github.com/JordanMarr/ReactiveElmish
F# fsharp Elmish MVU MVVM
- 1.0.0-beta
+ 1.1.0-beta.5
$(OtherFlags) --warnon:1182
@@ -25,7 +25,9 @@
+
+
diff --git a/src/ReactiveElmish/ReactiveElmishStore.fs b/src/ReactiveElmish/ReactiveElmishStore.fs
index bd13b0e..eff00c1 100644
--- a/src/ReactiveElmish/ReactiveElmishStore.fs
+++ b/src/ReactiveElmish/ReactiveElmishStore.fs
@@ -5,16 +5,23 @@ open System.Reactive.Subjects
open System.Reactive.Linq
open System
-type IStore<'Model, 'Msg> =
+type IStore<'Model> =
inherit IDisposable
- abstract member Dispatch: 'Msg -> unit
abstract member Model: 'Model with get
abstract member Observable: IObservable<'Model>
+type IStore<'Model, 'Msg> =
+ inherit IStore<'Model>
+ abstract member Dispatch: 'Msg -> unit
+
+type IHasSubject<'Model> =
+ abstract member Subject: Subject<'Model> with get
+
module Design =
/// Stubs a constructor injected dependency in design mode.
let stub<'T> = Unchecked.defaultof<'T>
+/// An Elmish reactive store that can be used to store and update a model and send out an Rx stream of the model.
type ReactiveElmishStore<'Model, 'Msg> () =
let _modelSubject = new Subject<'Model>()
let mutable _model: 'Model = Unchecked.defaultof<'Model>
@@ -26,7 +33,8 @@ type ReactiveElmishStore<'Model, 'Msg> () =
member this.Model = _model
member this.Observable = _modelSubject.AsObservable()
- member internal this.Subject = _modelSubject
+ interface IHasSubject<'Model> with
+ member this.Subject = _modelSubject
member this.Dispatcher
with get() = _dispatch
diff --git a/src/ReactiveElmish/ReactiveElmishViewModel.fs b/src/ReactiveElmish/ReactiveElmishViewModel.fs
index ac4d183..3310743 100644
--- a/src/ReactiveElmish/ReactiveElmishViewModel.fs
+++ b/src/ReactiveElmish/ReactiveElmishViewModel.fs
@@ -1,5 +1,4 @@
-#nowarn "0064" // FS0064: This construct causes code to be less generic than indicated by the type annotations.
-namespace ReactiveElmish
+namespace ReactiveElmish
open System.ComponentModel
open System.Reactive.Linq
@@ -12,19 +11,29 @@ open DynamicData
open System.Collections.ObjectModel
open ReactiveUI
-type ReactiveElmishViewModel() =
+[]
+module private Utils =
+ let readOnlyCollection<'T>() =
+ new ReadOnlyObservableCollection<'T>(new ObservableCollection<'T>())
+
+type ReactiveElmishViewModel(onPropertyChanged: string -> unit) =
inherit ReactiveUI.ReactiveObject()
let disposables = ResizeArray()
let propertySubscriptions = Dictionary()
+ let propertyCollections = Dictionary()
+
+ new() as this =
+ let opc propertyName = this.RaisePropertyChanged(propertyName)
+ new ReactiveElmishViewModel(opc)
/// Fires the `PropertyChanged` event for the given property name. Uses the caller's name if no property name is given.
member this.OnPropertyChanged([] ?propertyName: string) =
this.RaisePropertyChanged(propertyName.Value)
/// Binds a VM property to a `modelProjection` value and refreshes the VM property when the `modelProjection` value changes.
- member this.Bind<'Model, 'Msg, 'ModelProjection>(
- store: IStore<'Model, 'Msg>,
+ member this.Bind<'Model, 'ModelProjection>(
+ store: IStore<'Model>,
modelProjection: 'Model -> 'ModelProjection,
[] ?vmPropertyName
) =
@@ -36,7 +45,7 @@ type ReactiveElmishViewModel() =
.DistinctUntilChanged(modelProjection)
.Subscribe(fun _ ->
// Alerts the view that the 'Model projection / VM property has changed.
- this.OnPropertyChanged(vmPropertyName)
+ onPropertyChanged(vmPropertyName)
#if DEBUG
printfn $"PropertyChanged: {vmPropertyName} by {this}"
#endif
@@ -49,8 +58,8 @@ type ReactiveElmishViewModel() =
/// Binds a VM property to a `modelProjection` value and refreshes the VM property when the `onChanged` value changes.
/// The `modelProjection` function will only be called when the `onChanged` value changes.
/// `onChanged` usually returns a property value or a tuple of property values.
- member this.BindOnChanged<'Model, 'Msg, 'OnChanged, 'ModelProjection>(
- store: IStore<'Model, 'Msg>,
+ member this.BindOnChanged<'Model, 'OnChanged, 'ModelProjection>(
+ store: IStore<'Model>,
onChanged: 'Model -> 'OnChanged,
modelProjection: 'Model -> 'ModelProjection,
[] ?vmPropertyName
@@ -63,12 +72,18 @@ type ReactiveElmishViewModel() =
.DistinctUntilChanged(onChanged)
.Subscribe(fun x ->
// Alerts the view that the 'Model projection / VM property has changed.
- this.OnPropertyChanged(vmPropertyName)
+ onPropertyChanged(vmPropertyName)
#if DEBUG
printfn $"PropertyChanged: {vmPropertyName} by {this}"
#endif
)
- (store :?> ReactiveElmishStore<'Model, 'Msg>).Subject.OnNext(store.Model) // prime the pump
+
+ match store with
+ | :? IHasSubject<'Model> as subject ->
+ subject.Subject.OnNext(store.Model) // prime the pump
+ | _ ->
+ failwith "BindOnChanged requires the store to implement ISubject<'Model>"
+
propertySubscriptions.Add(vmPropertyName, disposable)
modelProjection store.Model
@@ -78,26 +93,31 @@ type ReactiveElmishViewModel() =
///
/// The reactive store to bind to.
/// The model projection.
- member this.BindList<'Model, 'Msg, 'ModelProjection>(
- store: IStore<'Model, 'Msg>,
+ member this.BindList<'Model, 'ModelProjection>(
+ store: IStore<'Model>,
modelProjectionSeq: 'Model -> 'ModelProjection seq,
[] ?vmPropertyName
) =
- let vmPropertyName = vmPropertyName.Value
- let mutable readOnlyList: ReadOnlyObservableCollection<'ModelProjection> = Unchecked.defaultof<_>
- if not (propertySubscriptions.ContainsKey vmPropertyName) then
+ if not (propertyCollections.ContainsKey vmPropertyName.Value) then
+ let mutable readOnlyList = readOnlyCollection<'ModelProjection>()
let sourceList = SourceList.createFrom (modelProjectionSeq store.Model)
- readOnlyList <- this.BindSourceList(sourceList, vmPropertyName)
-
- let disposable =
- store.Observable
- .Select(modelProjectionSeq)
- //.DistinctUntilChanged()
- .Subscribe(sourceList.EditDiff)
+ sourceList
+ .Connect()
+ .Bind(&readOnlyList)
+ .Subscribe()
+ |> this.AddDisposable
+
+ store.Observable
+ .Select(modelProjectionSeq)
+ //.DistinctUntilChanged()
+ .Subscribe(sourceList.EditDiff)
+ |> this.AddDisposable
this.AddDisposable(sourceList)
- this.AddDisposable(disposable)
- readOnlyList
+ propertyCollections.Add(vmPropertyName.Value, readOnlyList)
+ readOnlyList
+ else
+ propertyCollections[vmPropertyName.Value] :?> ReadOnlyObservableCollection<'ModelProjection>
///
/// Binds a model colleciton property to a DynamicData SourceList.
@@ -105,27 +125,35 @@ type ReactiveElmishViewModel() =
/// The reactive store to bind to.
/// The model projection.
/// A function that transforms each item in the collection when it is added to the SourceList.
- member this.BindList'<'Model, 'Msg, 'ModelProjection, 'Mapped>(
- store: IStore<'Model, 'Msg>,
+ member this.BindList<'Model, 'ModelProjection, 'Mapped>(
+ store: IStore<'Model>,
modelProjectionSeq: 'Model -> 'ModelProjection seq,
map: 'ModelProjection -> 'Mapped,
[] ?vmPropertyName
) =
let vmPropertyName = vmPropertyName.Value
- let mutable readOnlyList: ReadOnlyObservableCollection<'Mapped> = Unchecked.defaultof<_>
- if not (propertySubscriptions.ContainsKey vmPropertyName) then
+ if not (propertyCollections.ContainsKey vmPropertyName) then
+ let mutable readOnlyList = readOnlyCollection<'Mapped>()
let sourceList = SourceList.createFrom (modelProjectionSeq store.Model)
- readOnlyList <- this.BindSourceList(sourceList, map, vmPropertyName)
-
- let disposable =
- store.Observable
- .Select(modelProjectionSeq)
- //.DistinctUntilChanged()
- .Subscribe(sourceList.EditDiff)
+
+ sourceList
+ .Connect()
+ .Transform(map)
+ .Bind(&readOnlyList)
+ .Subscribe()
+ |> this.AddDisposable
+
+ store.Observable
+ .Select(modelProjectionSeq)
+ //.DistinctUntilChanged()
+ .Subscribe(sourceList.EditDiff)
+ |> this.AddDisposable
this.AddDisposable(sourceList)
- this.AddDisposable(disposable)
- readOnlyList
+ propertyCollections.Add(vmPropertyName, readOnlyList)
+ readOnlyList
+ else
+ propertyCollections[vmPropertyName] :?> ReadOnlyObservableCollection<'Mapped>
///
/// Binds a model Map<'Key, 'Value> property to an ObservableCollection.
@@ -136,22 +164,19 @@ type ReactiveElmishViewModel() =
/// A function that returns the identifier of the item.
/// An optional function that updates the transformed item when it is updated in the model. NOTE: This is expensive as it requires all items to be compared.
/// An optional function that returns a sort expression.
- member this.BindKeyedList<'Model, 'Msg, 'Key, 'Value, 'Mapped when 'Value : equality and 'Mapped : not struct and 'Key : comparison>(
- store: IStore<'Model, 'Msg>,
+ member this.BindKeyedList<'Model, 'Key, 'Value, 'Mapped when 'Value : equality and 'Mapped : not struct and 'Key : comparison>(
+ store: IStore<'Model>,
modelProjection: 'Model -> Map<'Key, 'Value>,
map: 'Value -> 'Mapped,
getKey: 'Mapped -> 'Key,
- ?update: 'Value -> 'Mapped -> unit,
- ?sortBy: 'Mapped -> IComparable,
+ ?update: Action<'Value, 'Mapped>,
+ ?sortBy: Func<'Mapped, IComparable>,
[] ?vmPropertyName
) =
let vmPropertyName = vmPropertyName.Value
- let mutable lastModelMap: Map<'Key, 'Value> = Unchecked.defaultof<_>
- let mutable observableCollection: ObservableCollection<'Mapped> = Unchecked.defaultof<_>
- if not (propertySubscriptions.ContainsKey vmPropertyName) then
- observableCollection <- ObservableCollection()
- lastModelMap <- Map.empty
-
+ if not (propertyCollections.ContainsKey vmPropertyName) then
+ let mutable lastModelMap: Map<'Key, 'Value> = Map.empty
+ let mutable observableCollection = ObservableCollection()
let mapToObservableCollection (currentModelMap: Map<'Key, 'Value>) =
// Apply updates to existing items (excluding new or removed items)
@@ -172,7 +197,7 @@ type ReactiveElmishViewModel() =
let newItem = currentModelMap[key]
let oldItem = lastModelMap[key]
if newItem <> oldItem then
- update newItem itemVM
+ update.Invoke(newItem, itemVM)
)
// Get items from the currentModelMap that are missing from the lastModelMap
@@ -216,7 +241,7 @@ type ReactiveElmishViewModel() =
sortBy
|> Option.iter (fun sortBy ->
observableCollection
- |> Seq.sortBy sortBy
+ |> Seq.sortBy sortBy.Invoke
|> Seq.iteri (fun idx item -> observableCollection[idx] <- item)
)
@@ -233,8 +258,11 @@ type ReactiveElmishViewModel() =
.DistinctUntilChanged()
.Subscribe(mapToObservableCollection)
- propertySubscriptions.Add(vmPropertyName, disposable)
- observableCollection
+ this.AddDisposable disposable
+ propertyCollections.Add(vmPropertyName, observableCollection)
+ observableCollection
+ else
+ propertyCollections[vmPropertyName] :?> ObservableCollection<'Mapped>
///
/// Binds a model Map<'Key, 'Value> property to an ObservableCollection.
@@ -244,12 +272,12 @@ type ReactiveElmishViewModel() =
/// A function that returns the identifier of the item.
/// An optional function that updates the transformed item when it is updated in the model. NOTE: This is expensive as it requires all items to be compared.
/// A function that returns a sort expression.
- member this.BindKeyedList<'Model, 'Msg, 'Key, 'Value when 'Value: equality and 'Value : not struct and 'Key : comparison>(
- store: IStore<'Model, 'Msg>,
+ member this.BindKeyedList<'Model, 'Key, 'Value when 'Value: equality and 'Value : not struct and 'Key : comparison>(
+ store: IStore<'Model>,
modelProjection: 'Model -> Map<'Key, 'Value>,
getKey: 'Value -> 'Key,
- ?update: 'Value -> 'Value -> unit,
- ?sortBy: 'Value -> IComparable,
+ ?update: Action<'Value, 'Value>,
+ ?sortBy,
[] ?vmPropertyName: string
) =
this.BindKeyedList(store = store, modelProjection = modelProjection, map = id, getKey = getKey, ?update = update, ?sortBy = sortBy, ?vmPropertyName = vmPropertyName)
@@ -260,16 +288,19 @@ type ReactiveElmishViewModel() =
[] ?vmPropertyName
) =
let vmPropertyName = vmPropertyName.Value
- let mutable readOnlyList: ReadOnlyObservableCollection<'T> = Unchecked.defaultof<_>
- if not (propertySubscriptions.ContainsKey vmPropertyName) then
+ if not (propertyCollections.ContainsKey vmPropertyName) then
+ let mutable readOnlyList = readOnlyCollection<'T>()
// Creates a subscription to a ISourceList<'T> and stores it in a dictionary.
let disposable =
sourceList
.Connect()
.Bind(&readOnlyList)
.Subscribe()
- propertySubscriptions.Add(vmPropertyName, disposable)
- readOnlyList
+ this.AddDisposable disposable
+ propertyCollections.Add(vmPropertyName, readOnlyList)
+ readOnlyList
+ else
+ propertyCollections[vmPropertyName] :?> ReadOnlyObservableCollection<'T>
/// Binds a VM property to a 'Model DynamicData.ISourceList<'T> property.
member this.BindSourceList<'T, 'Mapped>(
@@ -278,8 +309,8 @@ type ReactiveElmishViewModel() =
[] ?vmPropertyName
) =
let vmPropertyName = vmPropertyName.Value
- let mutable readOnlyList: ReadOnlyObservableCollection<'Mapped> = Unchecked.defaultof<_>
- if not (propertySubscriptions.ContainsKey vmPropertyName) then
+ if not (propertyCollections.ContainsKey vmPropertyName) then
+ let mutable readOnlyList = readOnlyCollection<'Mapped>()
// Creates a subscription to a ISourceList<'T> and stores it in a dictionary.
let disposable =
sourceList
@@ -287,18 +318,21 @@ type ReactiveElmishViewModel() =
.Transform(map)
.Bind(&readOnlyList)
.Subscribe()
- propertySubscriptions.Add(vmPropertyName, disposable)
- readOnlyList
+ this.AddDisposable disposable
+ propertyCollections.Add(vmPropertyName, readOnlyList)
+ readOnlyList
+ else
+ propertyCollections[vmPropertyName] :?> ReadOnlyObservableCollection<'Mapped>
/// Binds a VM property to a 'Model DynamicData.IObservableCache<'Value, 'Key> property.
member this.BindSourceCache<'Value, 'Key>(
sourceCache: IObservableCache<'Value, 'Key>,
- ?sortBy: 'Value -> 'IComparable,
+ ?sortBy: Func<'Value, IComparable>,
[] ?vmPropertyName
) =
let vmPropertyName = vmPropertyName.Value
- let mutable readOnlyList: ReadOnlyObservableCollection<'Value> = Unchecked.defaultof<_>
- if not (propertySubscriptions.ContainsKey vmPropertyName) then
+ if not (propertyCollections.ContainsKey vmPropertyName) then
+ let mutable readOnlyList = readOnlyCollection<'Value>()
// Creates a subscription to a SourceCache and stores it in a dictionary.
let disposable =
sourceCache
@@ -311,8 +345,11 @@ type ReactiveElmishViewModel() =
x.Sort(Comparer.Create(fun _ _ -> 0))
|> _.Bind(&readOnlyList)
|> _.Subscribe()
- propertySubscriptions.Add(vmPropertyName, disposable)
- readOnlyList
+ this.AddDisposable disposable
+ propertyCollections.Add(vmPropertyName, readOnlyList)
+ readOnlyList
+ else
+ propertyCollections[vmPropertyName] :?> ReadOnlyObservableCollection<'Value>
///
/// Binds a VM property to a 'Model DynamicData.IObservableCache<'Value, 'Key> property.
@@ -324,13 +361,13 @@ type ReactiveElmishViewModel() =
member this.BindSourceCache<'Value, 'Key, 'Mapped when 'Value : not struct and 'Mapped : not struct>(
sourceCache: IObservableCache<'Value, 'Key>,
map: 'Value -> 'Mapped,
- ?update: 'Value -> 'Mapped -> unit,
- ?sortBy: 'Mapped -> 'IComparable,
+ ?update: Action<'Value, 'Mapped>,
+ ?sortBy,
[] ?vmPropertyName
) =
let vmPropertyName = vmPropertyName.Value
- let mutable readOnlyList: ReadOnlyObservableCollection<'Mapped> = Unchecked.defaultof<_>
- if not (propertySubscriptions.ContainsKey vmPropertyName) then
+ if not (propertyCollections.ContainsKey vmPropertyName) then
+ let mutable readOnlyList = readOnlyCollection<'Mapped>()
// Creates a subscription to a SourceCache and stores it in a dictionary.
let disposable =
sourceCache
@@ -338,7 +375,7 @@ type ReactiveElmishViewModel() =
|> fun x ->
match update with
| Some update ->
- x.TransformWithInlineUpdate(map, fun mapped value -> update value mapped)
+ x.TransformWithInlineUpdate(map, fun mapped value -> update.Invoke(value, mapped))
| None ->
x.Transform(map)
|> fun x ->
@@ -349,8 +386,11 @@ type ReactiveElmishViewModel() =
x.Sort(Comparer.Create(fun _ _ -> 0))
|> _.Bind(&readOnlyList)
|> _.Subscribe()
- propertySubscriptions.Add(vmPropertyName, disposable)
- readOnlyList
+ this.AddDisposable disposable
+ propertyCollections.Add(vmPropertyName, readOnlyList)
+ readOnlyList
+ else
+ propertyCollections[vmPropertyName] :?> ReadOnlyObservableCollection<'Mapped>
/// Subscribes to an IObservable<> and adds the subscription to the list of disposables.
member this.Subscribe(observable: IObservable<'T>, handler: 'T -> unit) =
@@ -366,3 +406,4 @@ type ReactiveElmishViewModel() =
disposables |> Seq.iter _.Dispose()
propertySubscriptions.Values |> Seq.iter _.Dispose()
propertySubscriptions.Clear()
+
diff --git a/src/ReactiveElmish/ReactiveStore.fs b/src/ReactiveElmish/ReactiveStore.fs
new file mode 100644
index 0000000..26d5145
--- /dev/null
+++ b/src/ReactiveElmish/ReactiveStore.fs
@@ -0,0 +1,34 @@
+namespace ReactiveElmish
+
+open System.Reactive.Subjects
+open System.Reactive.Linq
+open System
+
+/// A reactive store that can be used to store and update a model and send out an Rx stream of the model.
+type ReactiveStore<'Model>(init: 'Model) =
+ let _modelSubject = new Subject<'Model>()
+ let mutable _model: 'Model = init
+
+ interface IStore<'Model> with
+ member this.Model = this.Model
+ member this.Observable = this.Observable
+
+ member this.Model = _model
+ member this.Observable = _modelSubject.AsObservable()
+
+ /// Updates the model and sends out an Rx stream.
+ member this.Update(fn: Func<'Model, 'Model>) =
+ _model <- fn.Invoke _model
+ _modelSubject.OnNext(_model)
+
+ /// Updates the model and sends out an Rx stream.
+ member this.Update(fn: 'Model -> 'Model) =
+ _model <- fn _model
+ _modelSubject.OnNext(_model)
+
+ interface IHasSubject<'Model> with
+ member this.Subject = _modelSubject
+
+ interface IDisposable with
+ member this.Dispose() =
+ _modelSubject.Dispose()
\ No newline at end of file
diff --git a/src/Samples/AvaloniaExample/ViewModels/CounterViewModel.fs b/src/Samples/AvaloniaExample/ViewModels/CounterViewModel.fs
index 15056dd..4269ab3 100644
--- a/src/Samples/AvaloniaExample/ViewModels/CounterViewModel.fs
+++ b/src/Samples/AvaloniaExample/ViewModels/CounterViewModel.fs
@@ -48,7 +48,7 @@ type CounterViewModel() =
|> Program.mkStore
member this.Count = this.Bind(local, _.Count)
- member this.Actions = this.BindList'(local, _.Actions, fun a -> { a with Description = $"** {a.Description} **" })
+ member this.Actions = this.BindList(local, _.Actions, map = fun a -> { a with Description = $"** {a.Description} **" })
member this.Increment() = local.Dispatch Increment
member this.Decrement() = local.Dispatch Decrement
member this.Reset() = local.Dispatch Reset
diff --git a/src/WpfExample/App.xaml b/src/WpfExample/App.xaml
new file mode 100644
index 0000000..99531b5
--- /dev/null
+++ b/src/WpfExample/App.xaml
@@ -0,0 +1,9 @@
+
+
+
+
+
diff --git a/src/WpfExample/App.xaml.cs b/src/WpfExample/App.xaml.cs
new file mode 100644
index 0000000..362247b
--- /dev/null
+++ b/src/WpfExample/App.xaml.cs
@@ -0,0 +1,14 @@
+using System.Configuration;
+using System.Data;
+using System.Windows;
+
+namespace WpfExample
+{
+ ///
+ /// Interaction logic for App.xaml
+ ///
+ public partial class App : Application
+ {
+ }
+
+}
diff --git a/src/WpfExample/AssemblyInfo.cs b/src/WpfExample/AssemblyInfo.cs
new file mode 100644
index 0000000..b0ec827
--- /dev/null
+++ b/src/WpfExample/AssemblyInfo.cs
@@ -0,0 +1,10 @@
+using System.Windows;
+
+[assembly: ThemeInfo(
+ ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
+ //(used if a resource is not found in the page,
+ // or application resource dictionaries)
+ ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
+ //(used if a resource is not found in the page,
+ // app, or any theme specific resource dictionaries)
+)]
diff --git a/src/WpfExample/MainWindow.xaml b/src/WpfExample/MainWindow.xaml
new file mode 100644
index 0000000..4cce1aa
--- /dev/null
+++ b/src/WpfExample/MainWindow.xaml
@@ -0,0 +1,13 @@
+
+
+
+
+
diff --git a/src/WpfExample/MainWindow.xaml.cs b/src/WpfExample/MainWindow.xaml.cs
new file mode 100644
index 0000000..84abf92
--- /dev/null
+++ b/src/WpfExample/MainWindow.xaml.cs
@@ -0,0 +1,24 @@
+using System.Text;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Data;
+using System.Windows.Documents;
+using System.Windows.Input;
+using System.Windows.Media;
+using System.Windows.Media.Imaging;
+using System.Windows.Navigation;
+using System.Windows.Shapes;
+
+namespace WpfExample
+{
+ ///
+ /// Interaction logic for MainWindow.xaml
+ ///
+ public partial class MainWindow : Window
+ {
+ public MainWindow()
+ {
+ InitializeComponent();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/WpfExample/ViewModels/CounterViewModel.cs b/src/WpfExample/ViewModels/CounterViewModel.cs
new file mode 100644
index 0000000..bb9c6b4
--- /dev/null
+++ b/src/WpfExample/ViewModels/CounterViewModel.cs
@@ -0,0 +1,52 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using CommunityToolkit.Mvvm.Input;
+using ReactiveElmish;
+
+
+using ReactiveUI;
+
+namespace WpfExample.ViewModels
+{
+ public class CounterStore() : ReactiveStore(CounterModel.Init())
+ {
+ public void Increment() =>
+ Update(m => m with { Count = m.Count + 1, Actions = m.Actions.Append(new Action("Increment", DateTime.Now)).ToArray() });
+
+ public void Decrement() =>
+ Update(m => m with { Count = m.Count - 1, Actions = m.Actions.Append(new Action("Decrement", DateTime.Now)).ToArray() });
+
+ public void Reset() =>
+ Update(m => CounterModel.Init());
+ }
+
+ public record CounterModel(
+ int Count,
+ Action[] Actions
+ )
+ {
+ public static CounterModel Init() => new CounterModel(0, [new Action("Initialized", DateTime.Now)]);
+ }
+
+ public record Action(
+ string Description,
+ DateTime Timestamp
+ );
+
+ public class CounterViewModel : ReactiveObject
+ {
+ readonly CounterStore store = new CounterStore();
+ public CounterViewModel() => Rx = new ReactiveBindingsCS(this.RaisePropertyChanged);
+ ReactiveBindingsCS Rx { get; }
+
+ public int Count => Rx.Bind(store, s => s.Count);
+ public ReadOnlyCollection Actions => Rx.BindList(store, s => s.Actions);
+ public IRelayCommand Increment { get => new RelayCommand(store.Increment); }
+ public IRelayCommand Decrement { get => new RelayCommand(store.Decrement); }
+ public IRelayCommand Reset { get => new RelayCommand(store.Reset); }
+ }
+}
diff --git a/src/WpfExample/Views/CounterView.xaml b/src/WpfExample/Views/CounterView.xaml
new file mode 100644
index 0000000..e90d5c9
--- /dev/null
+++ b/src/WpfExample/Views/CounterView.xaml
@@ -0,0 +1,27 @@
+
+
+
+
+ Counter:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/WpfExample/Views/CounterView.xaml.cs b/src/WpfExample/Views/CounterView.xaml.cs
new file mode 100644
index 0000000..8ec3730
--- /dev/null
+++ b/src/WpfExample/Views/CounterView.xaml.cs
@@ -0,0 +1,28 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Data;
+using System.Windows.Documents;
+using System.Windows.Input;
+using System.Windows.Media;
+using System.Windows.Media.Imaging;
+using System.Windows.Shapes;
+
+namespace WpfExample.Views
+{
+ ///
+ /// Interaction logic for CounterView.xaml
+ ///
+ public partial class CounterView : UserControl
+ {
+ public CounterView()
+ {
+ InitializeComponent();
+ DataContext = new ViewModels.CounterViewModel();
+ }
+ }
+}
diff --git a/src/WpfExample/WpfExample.csproj b/src/WpfExample/WpfExample.csproj
new file mode 100644
index 0000000..a399f81
--- /dev/null
+++ b/src/WpfExample/WpfExample.csproj
@@ -0,0 +1,19 @@
+
+
+
+ WinExe
+ net8.0-windows
+ enable
+ enable
+ true
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/publish.fsx b/src/publish.fsx
index 584b7c2..bfae96f 100644
--- a/src/publish.fsx
+++ b/src/publish.fsx
@@ -8,11 +8,29 @@ let src = __SOURCE_DIRECTORY__
pipeline "Publish" {
- stage "Build ReactiveElmish.Avalonia" {
+ stage "Restore and Build" {
run $"dotnet restore {src}/ReactiveElmish.Avalonia/ReactiveElmish.Avalonia.fsproj"
+ run $"dotnet build {src}/ReactiveElmish/ReactiveElmish.fsproj --configuration Release"
run $"dotnet build {src}/ReactiveElmish.Avalonia/ReactiveElmish.Avalonia.fsproj --configuration Release"
}
+ stage "Publish ReactiveElmish" {
+ run (fun ctx ->
+ let version =
+ let project = FileInfo $"{src}/ReactiveElmish/ReactiveElmish.fsproj"
+ match XDocument.Load(project.FullName).Descendants("Version") |> Seq.tryHead with
+ | Some versionElement -> versionElement.Value
+ | None -> failwith $"Could not find a element in '{project.Name}'."
+
+ let nugetKey =
+ match ctx.TryGetEnvVar "REACTIVE_ELMISH_NUGET_KEY" with
+ | ValueSome nugetKey -> nugetKey
+ | ValueNone -> failwith "The NuGet API key must be set in an 'REACTIVE_ELMISH_NUGET_KEY' environmental variable"
+
+ $"dotnet nuget push \"{src}/ReactiveElmish/bin/Release/ReactiveElmish.{version}.nupkg\" -s nuget.org -k {nugetKey} --skip-duplicate"
+ )
+ }
+
stage "Publish ReactiveElmish.Avalonia" {
run (fun ctx ->
let version =