【问题标题】:ReactiveUI for Xamarin Forms: Two-way binding doesn't work for custom BindablePropertyXamarin 表单的 ReactiveUI:双向绑定不适用于自定义 BindableProperty
【发布时间】:2017-01-15 23:27:23
【问题描述】:

我正在扩展 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 设置器就会被命中,而在地图平移时,Location 设置器不会被命中,从而导致其Center 属性的更新。

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

更新:这与 ReactiveUI 的 WhenAnyValue 扩展有关,它在我的绑定内部使用。为了证明这一点,我在视图创建中添加了几行代码:

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

【问题讨论】:

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


    【解决方案1】:

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

    【讨论】:

    • 这是一个很好的提示,我一定会实施。即使不能解决眼前的问题,也是对现有架构的改进。
    • 我进行了更改并相应地编辑了问题。正如我所怀疑的那样,它并没有解决问题,但它有助于清理代码。感谢您的建议。
    【解决方案2】:

    解决方案非常简单。

    我在没有调用基本实现的情况下覆盖 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 使用 Observable.FromEventPattern 将此事件转换为 IObservable

    【讨论】:

      猜你喜欢
      • 2015-01-06
      • 2017-04-01
      • 2018-10-21
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-11-05
      • 1970-01-01
      • 2018-05-14
      相关资源
      最近更新 更多