我正在扩展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.Location
和DashboardView.Map.Center
之间存在双向绑定。我在DashboardViewModel.SearchAddress
和DashboardView.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
答案 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
。