Xamarin表单的ReactiveUI:双向绑定不适用于自定义BindableProperty

时间:2017-01-15 23:27:23

标签: google-maps f# xamarin.forms reactiveui

我正在扩展Xamarin Forms Map类,它适用于MVVM架构。这是派生类型:

type GeographicMap() =
    inherit Map()
    static let centerProperty = BindableProperty.Create("Center", typeof<GeodesicLocation>, typeof<GeographicMap>, new GeodesicLocation())
    static let radiusProperty = BindableProperty.Create("Radius", typeof<float>, typeof<GeographicMap>, 1.0)
    member this.Radius
        with get() = 1.0<km> * (this.GetValue(radiusProperty) :?> float)
        and set(value: float<km>) = if not <| value.Equals(this.Radius) then this.SetValue(radiusProperty, value / 1.0<km>)
    member this.Center 
        with get() = this.GetValue(centerProperty) :?> GeodesicLocation
        and set(value: GeodesicLocation) = if not <| value.Equals(this.Center) then this.SetValue(centerProperty, value)
    override this.OnPropertyChanged(propertyName) =
        match propertyName with
        | "VisibleRegion" ->
            this.Center <- this.VisibleRegion.Center |> XamarinGeographic.geodesicLocation
            this.Radius <- this.VisibleRegion.Radius |> XamarinGeographic.geographicDistance
        | "Radius" | "Center" -> 
            match box this.VisibleRegion with
            | null -> this.MoveToRegion(MapSpan.FromCenterAndRadius(this.Center |> XamarinGeographic.position, this.Radius |> XamarinGeographic.distance))
            | _ ->
                let existingCenter, existingRadius = this.VisibleRegion.Center |> XamarinGeographic.geodesicLocation, this.VisibleRegion.Radius |> XamarinGeographic.geographicDistance
                let deltaCenter, deltaRadius = Geodesic.WGS84.Distance existingCenter (this.Center), existingRadius - this.Radius
                let threshold =  0.1 * this.Radius
                if Math.Abs(deltaRadius / 1.0<km>) > threshold / 1.0<km> || Math.Abs((deltaCenter |> UnitConversion.kilometres) / 1.0<km>) > threshold / 1.0<km> then
                    this.MoveToRegion(MapSpan.FromCenterAndRadius(this.Center |> XamarinGeographic.position, this.Radius |> XamarinGeographic.distance))
        | _ -> propertyName |> ignore

在我看来,我在Center属性和我的ViewModel的Location属性之间添加了一个绑定,如下所示:

type DashboardView(theme: Theme) as this = 
    inherit ContentPage<DashboardViewModel, DashboardView>(theme)
    new() = new DashboardView(Themes.AstridTheme)
    override __.CreateContent() =
        theme.GenerateGrid([|"Auto"; "*"|], [|"*"|]) |> withColumn(
            [|
                theme.VerticalLayout() |> withBlocks(
                    [|
                        theme.GenerateLabel(fun l -> this.Title <- l) 
                            |> withAlignment LayoutOptions.Center LayoutOptions.Center
                            |> withOneWayBinding(this.ViewModel, this, <@ fun (vm: DashboardViewModel) -> vm.Title @>, <@ fun (v: DashboardView) -> (v.Title: Label).Text @>)
                        theme.GenerateSearchBar(fun sb -> this.AddressSearchBar <- sb)
                            |> withSearchBarPlaceholder LocalisedStrings.SearchForAPlaceOfInterest
                            |> withTwoWayBinding(this.ViewModel, this, <@ fun (vm: DashboardViewModel) -> vm.SearchAddress @>, <@ fun (v: DashboardView) -> (v.AddressSearchBar: SearchBar).Text @>)
                            |> withSearchCommand this.ViewModel.SearchForAddress
                    |])
                theme.GenerateMap(fun m -> this.Map <- m)
                    |> withTwoWayBinding(this.ViewModel, this, <@ fun (vm: DashboardViewModel) -> vm.Location @>, <@ fun (v:DashboardView) -> (v.Map: GeographicMap).Center @>)
            |]) |> createFromColumns :> View
    member val AddressSearchBar = Unchecked.defaultof<SearchBar> with get, set
    member val Title = Unchecked.defaultof<Label> with get, set
    member val Map = Unchecked.defaultof<GeographicMap> with get, set

请注意,DashboardViewModel.LocationDashboardView.Map.Center之间存在双向绑定。我在DashboardViewModel.SearchAddressDashboardView.AddressSearchBar.Text之间也有双向约束。后者的约束力起作用;前者没有。我认为这一定是因为我没有正确设置可绑定属性GeographicMap.Center

我知道双向绑定无法正常工作,因为平移地图会导致VisibleRegion属性被修改,从而触发Center属性的更新。但是,在我的ViewModel类中:

type DashboardViewModel(?host: IScreen, ?platform: IPlatform) as this =
    inherit ReactiveViewModel()
    let host, platform = LocateIfNone host, LocateIfNone platform
    let searchResults = new ObservableCollection<GeodesicLocation>()
    let commandSubscriptions = new CompositeDisposable()
    let geocodeAddress(vm: DashboardViewModel) =
        let vm = match box vm with | null -> this | _ -> vm
        searchResults.Clear()
        async {
            let! results = platform.Geocoder.GetPositionsForAddressAsync(vm.SearchAddress) |> Async.AwaitTask
            results |> Seq.map (fun r -> new GeodesicLocation(r.Latitude * 1.0<deg>, r.Longitude * 1.0<deg>)) |> Seq.iter searchResults.Add
            match results |> Seq.tryLast with
            | Some position -> return position |> XamarinGeographic.geodesicLocation |> Some
            | None -> return None
        } |> Async.StartAsTask
    let searchForAddress = ReactiveCommand.CreateFromTask geocodeAddress
    let mutable searchAddress = String.Empty
    let mutable location = new GeodesicLocation(51.4<deg>, 0.02<deg>)
    override this.SubscribeToCommands() = searchForAddress.ObserveOn(RxApp.MainThreadScheduler).Subscribe(fun res -> match res with | Some l -> this.Location <- l | None -> res |> ignore) |> commandSubscriptions.Add
    override __.UnsubscribeFromCommands() = commandSubscriptions.Clear()
    member __.Title with get() = LocalisedStrings.AppTitle
    member __.SearchForAddress with get() = searchForAddress
    member this.SearchAddress 
        with get() = searchAddress 
        // GETS HIT WHEN SEARCH TEXT CHANGES
        and set(value) = this.RaiseAndSetIfChanged(&searchAddress, value, "SearchAddress") |> ignore
    member this.Location 
        with get() = location 
        // DOES NOT GET HIT WHEN THE MAP GETS PANNED, TRIGGERING AN UPDATE OF ITS Center PROPERTY
        and set(value) = this.RaiseAndSetIfChanged(&location, value, "Location") |> ignore
    interface IRoutableViewModel with
        member __.HostScreen = host
        member __.UrlPathSegment = "Dashboard"
每当更新搜索文本时,SearchAddress setter就会被点击,而当地图被淘汰时,Location setter不会被点击,从而导致更新其Center属性。< / p>

我是否遗漏了与我的可绑定Center属性设置有关的内容?

UPDATE :这与ReactiveUI的WhenAnyValue扩展程序有关,该扩展程序在我的绑定中内部使用。为了证明这一点,我在View创建中添加了几行:

override __.CreateContent() =
    let result = 
        theme.GenerateGrid([|"Auto"; "*"|], [|"*"|]) |> withColumn(
            [|
                theme.VerticalLayout() |> withBlocks(
                    [|
                        theme.GenerateLabel(fun l -> this.Title <- l) 
                            |> withAlignment LayoutOptions.Center LayoutOptions.Center
                            |> withOneWayBinding(this.ViewModel, this, <@ fun (vm: DashboardViewModel) -> vm.Title @>, <@ fun (v: DashboardView) -> (v.Title: Label).Text @>)
                        theme.GenerateSearchBar(fun sb -> this.AddressSearchBar <- sb)
                            |> withSearchBarPlaceholder LocalisedStrings.SearchForAPlaceOfInterest
                            |> withTwoWayBinding(this.ViewModel, this, <@ fun (vm: DashboardViewModel) -> vm.SearchAddress @>, <@ fun (v: DashboardView) -> (v.AddressSearchBar: SearchBar).Text @>)
                            |> withSearchCommand this.ViewModel.SearchForAddress
                    |])
                theme.GenerateMap(fun m -> this.Map <- m)
                    |> withTwoWayBinding(this.ViewModel, this, <@ fun (vm: DashboardViewModel) -> vm.Location @>, <@ fun (v:DashboardView) -> (v.Map: GeographicMap).Center @>)
            |]) |> createFromColumns :> View
    this.WhenAnyValue(ExpressionConversion.toLinq <@ fun (v:DashboardView) -> (v.Map: GeographicMap).Center @>).ObserveOn(RxApp.MainThreadScheduler).Subscribe(fun (z) ->
        z |> ignore) |> ignore // This breakpoint doesn't get hit when the map pans.
    this.WhenAnyValue(ExpressionConversion.toLinq <@ fun (v:DashboardView) -> (v.AddressSearchBar: SearchBar).Text @>).ObserveOn(RxApp.MainThreadScheduler).Subscribe(fun (z) ->
        z |> ignore) |> ignore // This breakpoint gets hit when text is changed in the search bar.
    result

2 个答案:

答案 0 :(得分:1)

您不应该在BindableProperty的get和set定义中进行任何其他操作而不是GetValue()和SetValue()调用。为了在设置或更改此属性时进行其他更改,您可以覆盖OnPropertyChanged方法并在那里进行必要的操作。

答案 1 :(得分:0)

解决方案非常简单。

我覆盖了OnPropertyChanged而没有调用基础实现,这会触发公共PropertyChanged事件:

protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
    PropertyChangedEventHandler handler = PropertyChanged;
    if (handler != null)
        handler(this, new PropertyChangedEventArgs(propertyName));
}

所以我需要做的是将base.OnPropertyChanged()的调用添加到我的覆盖:

type GeographicMap() =
    inherit Map()
    static let centerProperty = BindableProperty.Create("Center", typeof<GeodesicLocation>, typeof<GeographicMap>, new GeodesicLocation())
    static let radiusProperty = BindableProperty.Create("Radius", typeof<float>, typeof<GeographicMap>, 1.0)
    member this.Radius
        with get() = 1.0<km> * (this.GetValue(radiusProperty) :?> float)
        and set(value: float<km>) = if not <| value.Equals(this.Radius) then this.SetValue(radiusProperty, value / 1.0<km>)
    member this.Center 
        with get() = this.GetValue(centerProperty) :?> GeodesicLocation
        and set(value: GeodesicLocation) = if not <| value.Equals(this.Center) then this.SetValue(centerProperty, value)
    override this.OnPropertyChanged(propertyName) =
        base.OnPropertyChanged(propertyName)
        match propertyName with
        | "VisibleRegion" ->
            this.Center <- this.VisibleRegion.Center |> XamarinGeographic.geodesicLocation
            this.Radius <- this.VisibleRegion.Radius |> XamarinGeographic.geographicDistance
        | "Radius" | "Center" -> 
            match box this.VisibleRegion with
            | null -> this.MoveToRegion(MapSpan.FromCenterAndRadius(this.Center |> XamarinGeographic.position, this.Radius |> XamarinGeographic.distance))
            | _ ->
                let existingCenter, existingRadius = this.VisibleRegion.Center |> XamarinGeographic.geodesicLocation, this.VisibleRegion.Radius |> XamarinGeographic.geographicDistance
                let deltaCenter, deltaRadius = Geodesic.WGS84.Distance existingCenter (this.Center), existingRadius - this.Radius
                let threshold =  0.1 * this.Radius
                if Math.Abs(deltaRadius / 1.0<km>) > threshold / 1.0<km> || Math.Abs((deltaCenter |> UnitConversion.kilometres) / 1.0<km>) > threshold / 1.0<km> then
                    this.MoveToRegion(MapSpan.FromCenterAndRadius(this.Center |> XamarinGeographic.position, this.Radius |> XamarinGeographic.distance))
        | _ -> propertyName |> ignore

此更改允许公开事件触发。 ReactiveUI使用IObservable将此事件转换为Observable.FromEventPattern