从 ViewModel 绑定到 Xamarin.Forms.Maps.Map

Posted

技术标签:

【中文标题】从 ViewModel 绑定到 Xamarin.Forms.Maps.Map【英文标题】:Bind to Xamarin.Forms.Maps.Map from ViewModel 【发布时间】:2015-03-21 18:58:30 【问题描述】:

我正在使用显示地图的页面开发 Xamarin.Forms 应用程序。 XAML 是:

<maps:Map x:Name="Map">
    ...
</maps:Map>

我知道可以从页面的代码隐藏中访问地图,如下所示:

var position = new Position(37.79762, -122.40181);
Map.MoveToRegion(new MapSpan(position, 0.01, 0.01));
Map.Pins.Add(new Pin

    Label = "Xamarin",
    Position = position
);

但是因为这段代码会破坏应用程序的 MVVM 架构,我宁愿从我的 ViewModel 访问 Map 对象,而不是直接从视图/页面访问 - 可以像上面的代码一样直接使用它,也可以通过数据绑定到它的属性。

有人知道如何做到这一点吗?

【问题讨论】:

【参考方案1】:

如果您不想破坏 MVVM 模式并且仍然能够从 ViewModel 访问您的 Map 对象,那么您可以使用 ViewModel 中的属性公开 Map 实例并从您的 View 绑定到它。

您的代码的结构应如下所述。

视图模型:

using Xamarin.Forms.Maps;

namespace YourApp.ViewModels

    public class MapViewModel
    
        public MapViewModel()
        
            Map = new Map();
        

        public Map Map  get; private set; 
    

视图(在这个例子中我使用ContentPage,但你可以使用任何你喜欢的):

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="YourApp.Views.MapView">
    <ContentPage.Content>
                <!--The map-->
                <ContentView Content="Binding Map"/>
    </ContentPage.Content>
</ContentPage>

我没有展示如何,但是上面的代码只有在 ViewModel 是你的视图的BindingContext 时才能工作。

【讨论】:

这应该是公认的答案。对于那些不明白的人——通过绑定到一个属性,我们仍然受益于 XAML 基于类型来表示事物的方式。比其他任何解决方案都好得多。 完美解决方案。谢谢@Aquablue【参考方案2】:

创建一个新的控件怎么样?比如说BindableMap,它继承自 Map 并执行原始Map 内部缺乏的绑定更新。实现非常简单,我已经包含了 2 个基本需求; Pins 属性和当前 MapSpan。显然,您可以在此控件中添加自己的特殊需求。之后您所要做的就是将 ObservableCollection&lt;Pin&gt; 类型的属性添加到您的 ViewModel 并将其绑定到 XAML 中 BindableMap 的 PinsSource 属性。

这是 BindableMap:

public class BindableMap : Map


    public BindableMap()
    
        PinsSource = new ObservableCollection<Pin>();
        PinsSource.CollectionChanged += PinsSourceOnCollectionChanged;
    

    public ObservableCollection<Pin> PinsSource
    
        get  return (ObservableCollection<Pin>)GetValue(PinsSourceProperty); 
        set  SetValue(PinsSourceProperty, value); 
    

    public static readonly BindableProperty PinsSourceProperty = BindableProperty.Create(
                                                     propertyName: "PinsSource",
                                                     returnType: typeof(ObservableCollection<Pin>),
                                                     declaringType: typeof(BindableMap),
                                                     defaultValue: null,
                                                     defaultBindingMode: BindingMode.TwoWay,
                                                     validateValue: null,
                                                     propertyChanged: PinsSourcePropertyChanged);


    public MapSpan MapSpan
    
        get  return (MapSpan)GetValue(MapSpanProperty); 
        set  SetValue(MapSpanProperty, value); 
    

    public static readonly BindableProperty MapSpanProperty = BindableProperty.Create(
                                                     propertyName: "MapSpan",
                                                     returnType: typeof(MapSpan),
                                                     declaringType: typeof(BindableMap),
                                                     defaultValue: null,
                                                     defaultBindingMode: BindingMode.TwoWay,
                                                     validateValue: null,
                                                     propertyChanged: MapSpanPropertyChanged);

    private static void MapSpanPropertyChanged(BindableObject bindable, object oldValue, object newValue)
    
        var thisInstance = bindable as BindableMap;
        var newMapSpan = newValue as MapSpan;

        thisInstance?.MoveToRegion(newMapSpan);
    
    private static void PinsSourcePropertyChanged(BindableObject bindable, object oldvalue, object newValue)
    
        var thisInstance = bindable as BindableMap;
        var newPinsSource = newValue as ObservableCollection<Pin>;

        if (thisInstance == null ||
            newPinsSource == null)
            return;

        UpdatePinsSource(thisInstance, newPinsSource);
    
    private void PinsSourceOnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    
        UpdatePinsSource(this, sender as IEnumerable<Pin>);
    

    private static void UpdatePinsSource(Map bindableMap, IEnumerable<Pin> newSource)
    
        bindableMap.Pins.Clear();
        foreach (var pin in newSource)
            bindableMap.Pins.Add(pin);
    

注意事项:

为简单起见,我省略了 using 语句和命名空间声明。 为了在我们将成员添加到可绑定的PinsSource 属性时更新我们的原始Pins 属性,我将PinsSource 声明为ObservableCollection&lt;Pin&gt; 并订阅了它的CollectionChanged 事件。显然,如果您打算只更改绑定属性的整个值,则可以将其定义为 IList

关于这个问题的 2 个第一个答案,我的最后一句话:

虽然将 View 控件作为 ViewModel 属性可以让我们免于在代码中编写业务逻辑,但它仍然感觉有点 hacky。在我看来,MVVM 的 VM 部分(嗯,至少是关键点)的全部意义在于它与 V 完全分离并解耦。而上述答案中提供的解决方案实际上是这样的:

将 View Control 插入 ViewModel 的核心。

我认为这样,不仅你打破了 MVVM 模式,而且你也伤了它的心!

【讨论】:

我认为您的回答在这里最有意义。我实现了这一点,但在 SetValue(PinsSourceProperty, value); 中没有得到任何值。我在视图中使用 并且 mappins 是在 VM 内设置的 IList 。我做错了吗? @MickeyDhami 你的虚拟机是否实现了INotifyPropertyChanged,你的IList&lt;Pin&gt; 属性是否在其NotifyPropertyChanged() 部分调用NotifyPropertyChanged() @Pejman 我知道这篇文章很久以前了,但是如何使用自定义弹窗实现自定义引脚? @Stefan0309 好吧,基本上每当您想要自定义 Xamarin.Forms 组件超出控件提供的属性时,您有 2 个选择:1) 寻找满足您需求的第三方控件或 2)为控件创建一个CustomRenderer。如果你不熟悉CustomRenderer,你可以开始here。 我和@MickeyDhami 有同样的问题。有人可以帮我吗?【参考方案3】:

我有两个选项对我有用并且可以帮助你。

    您可以将静态Xamarin.Forms.Maps Map 属性添加到您的ViewModel 并在设置绑定上下文后设置此静态属性,在您的视图实例化期间,如下所示:

    public MapsPage()
        
            InitializeComponent();
            BindingContext = new MapViewModel();
            MapViewModel.Map = MyMap;
        
    

这将允许您在 ViewModel 中访问您的地图。

    您可以在绑定期间将您的地图从您的视图传递给 ViewModel,例如:

    <maps:Map
        x:Name="MyMap"
        IsShowingUser="true"
        MapType="Hybrid" />
    <StackLayout Orientation="Horizontal" HorizontalOptions="Center">
        <Button x:Name="HybridButton" Command="Binding MapToHybridViewChangeCommand" 
                CommandParameter="x:Reference MyMap"
                Text="Hybrid" HorizontalOptions="Center" VerticalOptions="Center" Margin="5"/>`
    

并从 ViewModel 的命令中获取地图。

【讨论】:

【参考方案4】:

我不认为 PinsMap 上的可绑定属性,您可能希望在 Xamarin 的 Uservoice 或此处的四个地址处提交功能请求:http://forums.xamarin.com/discussion/31273/

【讨论】:

【参考方案5】:

是的,Map.Pins 是不可绑定的,但是有ItemsSource,而是easy to use。

   <maps:Map ItemsSource="Binding Locations">
        <maps:Map.ItemTemplate>
            <DataTemplate>
                <maps:Pin Position="Binding Position"
                          Label="Binding Name"
                          Address="Binding Subtitle" />

因此,仅对于引脚,MVVM 可以在没有任何自定义控件的情况下完成。 但Map.MoveToRegion()(和Map.VisibleRegion 阅读)仍然开放。应该有办法绑定它们。为什么不在一个读/写属性中? (答案:因为无限循环。)

注意:如果你在启动时只需要Map.MoveToRegion一次,可以在构造函数中设置区域。

【讨论】:

【参考方案6】:

这并不理想,但您可以在后面的代码中侦听属性更改事件,然后从那里应用更改。它有点手动,但它是可行的。

((ViewModels.YourViewModel)BindingContext).PropertyChanged += yourPropertyChanged;

然后定义“yourPropertyChanged”方法

private void yourPropertyChanged(object sender, PropertyChangedEventArgs e)

    if(e.PropertyName == "YourPropertyName")
    
        var position = new Position(37.79762, -122.40181);
        Map.MoveToRegion(new MapSpan(position, 0.01, 0.01));
        Map.Pins.Add(new Pin
        
            Label = "Xamarin",
            Position = position
        );
    

【讨论】:

以上是关于从 ViewModel 绑定到 Xamarin.Forms.Maps.Map的主要内容,如果未能解决你的问题,请参考以下文章

从 ViewModel 绑定到 Xamarin.Forms.Maps.Map

将事件绑定到 ViewModel

将 CheckListBox 绑定到 ViewModel

MVVM从TabControl绑定到Page.DataContext

WPF 将 UI 事件绑定到 ViewModel 中的命令

从javascript函数更新Knockout viewmodel表单输入绑定