Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#157 one way seq via one way seq lazy #258

Merged
merged 9 commits into from
Aug 11, 2020
1 change: 1 addition & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* **Breaking:** Removed all occurrences of the argument `wrapDispatch` from the methods used to create a binding. There is currently no migration path. Please create an issue if this is a negative impact for you.
* **Breaking:** App initialization is now done using the `WpfProgram` module instead of the `Program` module
* **Breaking:** Removed `ElmConfig`. For controlling logging, see below. For specifying a binding performance log threshold (corresponding to the old `ElmConfig.MeasureLimitMs` field), use `WpfProgram.withBindingPerformanceLogThreshold`
* **Breaking:** The method `Binding.oneWaySeq` is implemented by calling the method `Binding.oneWaySeqLazy` with `equals` = `refEq` and `map` = `id`. This is a breaking change when using a mutable data structure for the sequence. Compensate by directly calling `Binding.oneWaySeqLazy` with `equals` = `fun _ _ = false`.
* Added binding mapping functions
* Added `mapModel`, `mapMsg`, and `mapMsgWithModel` in both the `Binding` and `Bindings` modules
* These functions enable common model and message mapping logic to be extracted
Expand Down
37 changes: 26 additions & 11 deletions src/Elmish.WPF.Tests/BindingTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -355,37 +355,52 @@ module oneWaySeq =


[<Fact>]
let ``final get passes through model`` () =
let ``final get returns value from original get`` () =
Property.check <| property {
let! x = GenX.auto<int>

let get = string
let d = Binding.oneWaySeq(get, fail2, fail) |> getOneWaySeqLazyData

test <@ d.Get x |> unbox = get x @>
}


[<Fact>]
let ``final map returns the seq its given`` () =
Property.check <| property {
let! array = Gen.guid |> Gen.array (Range.constant 1 50)

let list = array |> Array.toList
let d = Binding.oneWaySeq(fail, fail2, fail) |> getOneWaySeqLazyData

test <@ d.Get x |> unbox = x @>
test <@ list |> Seq.map box |> box |> d.Map |> Seq.map unbox |> Seq.toList = list @>
}


[<Fact>]
let ``final map returns value from original get`` () =
let ``final equals returns true for the same sequence references`` () =
Property.check <| property {
let! x = GenX.auto<string>
let! array = Gen.guid |> Gen.array (Range.constant 1 50)

let get : string -> char list = Seq.toList
let d = Binding.oneWaySeq(get, fail2, fail) |> getOneWaySeqLazyData
let list = array |> Seq.map box |> Seq.toList
let d = Binding.oneWaySeq(fail, fail2, fail) |> getOneWaySeqLazyData

test <@ d.Map (box x) |> Seq.map unbox |> Seq.toList = get x @>
test <@ d.Equals (box list) (box list) = true @>
}


[<Fact>]
let ``final equals returns false`` () =
let ``final equals returns false for different reference sequences`` () =
Property.check <| property {
let! x = GenX.auto<int>
let! y = GenX.auto<int>
let! array = Gen.guid |> Gen.array (Range.constant 1 50)

let list1 = array |> Seq.map box |> Seq.toList
let list2 = list1 |> Seq.map id |> Seq.toList
let d = Binding.oneWaySeq(fail, fail2, fail) |> getOneWaySeqLazyData

test <@ d.Equals (box x) (box y) = false @>
test <@ refEq list1 list2 = false @> // ensure lists are not reference equal
test <@ d.Equals (box list1) (box list2) = false @>
}


Expand Down
69 changes: 33 additions & 36 deletions src/Elmish.WPF/Binding.fs
Original file line number Diff line number Diff line change
Expand Up @@ -426,42 +426,12 @@ type Binding private () =
/// <summary>
/// Creates a one-way binding to a sequence of items, each uniquely
/// identified by the value returned by <paramref name="getId"/>. The
/// binding is backed by a persistent <c>ObservableCollection</c>, so only
/// changed items (as determined by <paramref name="itemEquals" />) will be
/// replaced. If the items are complex and you want them updated instead of
/// replaced, consider using <see cref="subModelSeq" />.
/// </summary>
/// <param name="get">Gets the collection from the model.</param>
/// <param name="itemEquals">
/// Indicates whether two collection items are equal. Good candidates are
/// <c>elmEq</c>, <c>refEq</c>, or simply <c>(=)</c>.
/// </param>
/// <param name="getId">Gets a unique identifier for a collection
/// item.</param>
static member oneWaySeq
(get: 'model -> #seq<'a>,
itemEquals: 'a -> 'a -> bool,
getId: 'a -> 'id)
: string -> Binding<'model, 'msg> =
OneWaySeqLazyData {
Get = box
Map = unbox<'model> >> get >> Seq.map box
Equals = fun _ _ -> false
GetId = unbox<'a> >> getId >> box
ItemEquals = fun a b -> itemEquals (unbox<'a> a) (unbox<'a> b)
} |> createBinding


/// <summary>
/// Creates a one-way binding to a sequence of items, each uniquely
/// identified by the value returned by <paramref name="getId"/>. The
/// binding will only be updated if the output of <paramref name="get" />
/// changes, as determined by <paramref name="equals" />. The binding is
/// backed by a persistent
/// <c>ObservableCollection</c>, so only changed items (as determined by
/// <paramref name="itemEquals" />) will be replaced. If the items are
/// complex and you want them updated instead of replaced, consider using
/// <see cref="subModelSeq" />.
/// binding will not be updated if the output of <paramref name="get"/>
/// does not change, as determined by <paramref name="equals"/>.
/// The binding is backed by a persistent <c>ObservableCollection</c>, so
/// only changed items (as determined by <paramref name="itemEquals"/>)
/// will be replaced. If the items are complex and you want them updated
/// instead of replaced, consider using <see cref="subModelSeq"/>.
/// </summary>
/// <param name="get">Gets the intermediate value from the model.</param>
/// <param name="equals">
Expand Down Expand Up @@ -490,6 +460,33 @@ type Binding private () =
ItemEquals = fun x y -> itemEquals (unbox<'b> x) (unbox<'b> y)
} |> createBinding


/// <summary>
/// Creates a one-way binding to a sequence of items, each uniquely
/// identified by the value returned by <paramref name="getId"/>. The
/// binding will not be updated if the output of <paramref name="get"/>
/// is referentially equal. This is the same as calling
/// <see cref="oneWaySeqLazy"/> with <c>equals = refEq</c> and
/// <c>map = id</c>. The binding is backed by a persistent
/// <c>ObservableCollection</c>, so only changed items (as determined by
/// <paramref name="itemEquals"/>) will be replaced. If the items are
/// complex and you want them updated instead of replaced, consider using
/// <see cref="subModelSeq"/>.
/// </summary>
/// <param name="get">Gets the collection from the model.</param>
/// <param name="itemEquals">
/// Indicates whether two collection items are equal. Good candidates are
/// <c>elmEq</c>, <c>refEq</c>, or simply <c>(=)</c>.
/// </param>
/// <param name="getId">Gets a unique identifier for a collection
/// item.</param>
static member oneWaySeq
(get: 'model -> #seq<'a>,
itemEquals: 'a -> 'a -> bool,
getId: 'a -> 'id)
: string -> Binding<'model, 'msg> =
Binding.oneWaySeqLazy(get, refEq, id, itemEquals, getId)


/// <summary>Creates a two-way binding.</summary>
/// <param name="get">Gets the value from the model.</param>
Expand Down
2 changes: 1 addition & 1 deletion src/Elmish.WPF/Elmish.WPF.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@
<Compile Include="AssemblyInfo.fs" />
<Compile Include="InternalUtils.fs" />
<Compile Include="InternalTypes.fs" />
<Compile Include="Binding.fs" />
<Compile Include="Utils.fs" />
<Compile Include="Binding.fs" />
<Compile Include="ViewModel.fs" />
<Compile Include="ViewModelModule.fs" />
<Compile Include="WpfProgram.fs" />
Expand Down