2017-01-15 7 views
1

私はMVRアーキテクチャに準拠したXamarin Forms Mapクラスの拡張を行っています。ここでは派生型である:私の見解ではReactUI for Xamarin Forms:カスタムBindablePropertyに対して双方向バインディングが機能しない

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クラスに:検索テキストが更新されるたびにマップがパンされるときLocationセッターがヒットされませんが

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セッターは、そのCenterプロパティの更新を引き起こし、ヒットを取得します。

バインド可能なCenterプロパティの設定に関連するものがありませんか?

UPDATE:これは、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 

答えて

0

解決策は非常に簡単でした。

私は公共PropertyChangedイベントを発生ベースの実装、まで呼び出すことなくOnPropertyChangedをオーバーライドした

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 

この変更により、公開イベントが発生します。このイベントはObservable.FromEventPatternを使用してReactiveUIによってIObservableに変換されます。

1

あなたが他の操作ではなくGetValueメソッド()とのSetValue()をごBindablePropertyのgetとsetの定義に呼び出しを行うべきではありません。このプロパティが設定または変更されたときに追加の変更を行うには、OnPropertyChangedメソッドをオーバーライドして必要な操作を行うことができます。

+0

これは良いヒントです。それは私が確かに実装するものです。すぐに問題が解決しなくても、既存のアーキテクチャの改善です。 –

+0

私は変更を加え、それに応じて質問を編集しました。私が疑っているように、それは問題を修正していませんが、それはコードを整理するのを助けました。提案していただきありがとうございます。 –

関連する問題