zoukankan      html  css  js  c++  java
  • 在 Windows Phone 中结合使用 OData 和 MVVM

    安装Odata Windows phone客户端
    http://msdn.microsoft.com/en-us/jj658961

    新建Windows Phone 8项目


    移除SampleData

    添加服务引用

    输入http://services.odata.org/Northwind/Northwind.svc/
    转到

    这会向该项目添加一个新的代码文件,该文件包含的数据类用来以对象形式访问数据服务资源并与之进行交互。这些数据类在命名空间 MVVMODataTestApp.Northwind 中创建。此外,还会添加对 System.Data.Services.Client.WP80.dll 程序集的引用。
    用以下代码替换ViewModels/MainViewModel.cs文件
    
    
    View Code
    using System;
    
    using System.Collections.ObjectModel; 
    
    using System.ComponentModel; 
    
    using MVVMODataTestApp.Resources; 
    
    using MVVMODataTestApp.Northwind; 
    
    using System.Data.Services.Client; 
    
    using System.Linq; 
    
    namespace MVVMODataTestApp.ViewModels 
    
    { 
    
    public class MainViewModel : INotifyPropertyChanged
    
     { 
    
    // This is the URI of the public, read-only Northwind data service. 
    
    // To make updates and save changes, replace this URI 
    
    // with your own Northwind service implementation.
    
    private static readonly Uri _rootUri = 
    
    new Uri("http://services.odata.org/Northwind/Northwind.svc/"); 
    
     
    
    // Define the typed DataServiceContext.
    
    private NorthwindEntities _context; 
    
     
    
    // Define the binding collection for Customers.
    
    private DataServiceCollection<Customer> _customers; 
    
     
    
    // Gets and sets the collection of Customer objects from the feed.
    
    // This collection is used to bind to the UI (View).
    
    public DataServiceCollection<Customer> Customers 
    
     { 
    
    get { return _customers; } 
    
     
    
    private set
    
     { 
    
    // Set the Titles collection.
    
     _customers = value; 
    
     
    
    // Register a handler for the LoadCompleted callback.
    
     _customers.LoadCompleted += OnCustomersLoaded; 
    
     
    
    // Raise the PropertyChanged events.
    
     NotifyPropertyChanged("Customers"); 
    
     } 
    
     } 
    
     
    
    // Used to determine whether the data is loaded.
    
    public bool IsDataLoaded { get; private set; } 
    
     
    
    // Loads data when the application is initialized.
    
    public void LoadData() 
    
     { 
    
    // Instantiate the context and binding collection.
    
     _context = new NorthwindEntities(_rootUri); 
    
     Customers = new DataServiceCollection<Customer>(_context); 
    
     
    
    // Specify an OData query that returns all customers.
    
    var query = from cust in _context.Customers 
    
    select cust; 
    
     
    
    // Load the customer data.
    
     Customers.LoadAsync(query); 
    
     } 
    
     
    
    // Displays data from the stored data context and binding collection 
    
    public void LoadData(NorthwindEntities context, 
    
    DataServiceCollection<Customer> _customers) 
    
     { 
    
     _context = context; 
    
     Customers = _customers; 
    
     
    
     IsDataLoaded = true; 
    
     } 
    
     
    
    // Handles the DataServiceCollection<T>.LoadCompleted event.
    
    private void OnCustomersLoaded(object sender, LoadCompletedEventArgs e) 
    
     { 
    
    // Make sure that we load all pages of the Customers feed.
    
    if (Customers.Continuation != null) 
    
     { 
    
     Customers.LoadNextPartialSetAsync(); 
    
     } 
    
     IsDataLoaded = true; 
    
     } 
    
     
    
    // Declare a PropertyChanged for the UI to register 
    
    // to get updates from the ViewModel.
    
    public event PropertyChangedEventHandler PropertyChanged; 
    
     
    
    // Notifies the binding about a changed property value.
    
    private void NotifyPropertyChanged(string propertyName) 
    
     { 
    
    var propertyChanged = PropertyChanged; 
    
    if (propertyChanged != null) 
    
     { 
    
    // Raise the PropertyChanged event.
    
     propertyChanged(this, new PropertyChangedEventArgs(propertyName)); 
    
     } 
    
     } 
    
     } 
    
    }
    
    

    更新MainPage.xaml文件

    View Code
    <phone:PhoneApplicationPage
    
     x:Class="MVVMODataTestApp.MainPage"
    
     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    
     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    
     xmlns:phone="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone"
    
     xmlns:shell="clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone"
    
     xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    
     xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    
     mc:Ignorable="d"
    
     FontFamily="{StaticResource PhoneFontFamilyNormal}"
    
     FontSize="{StaticResource PhoneFontSizeNormal}"
    
     Foreground="{StaticResource PhoneForegroundBrush}"
    
     SupportedOrientations="Portrait"  Orientation="Portrait"
    
     shell:SystemTray.IsVisible="True">
    
     
    
    <!--数据上下文设置为上面的示例数据,并且 LayoutRoot 包含根网格,其他所有页面内容都位于根网格中-->
    
    <Grid x:Name="LayoutRoot" Background="Transparent">
    
    <Grid.RowDefinitions>
    
    <RowDefinition Height="Auto"/>
    
    <RowDefinition Height="*"/>
    
    </Grid.RowDefinitions>
    
     
    
    <!-- 本地化说明:
    
    若要本地化显示的字符串,请将其值复制到应用程序的非特定语言资源文件(AppResources.resx)
    
    中的适当命名的键,然后
    
     将属性的引号之间的硬编码文本值
    
     替换为其路径指向该字符串名称的绑定子句。
    
     
    
    例如:
    
     
    
    Text="{Binding Path=LocalizedResources.ApplicationTitle, Source={StaticResource LocalizedStrings}}"
    
     
    
    此绑定指向模板的名为"ApplicationTitle"的字符串资源。
    
     
    
    在"项目属性"选项卡中添加受支持的语言将会为
    
     每种语言创建一个新的 resx 文件,该文件可以包含 UI 字符串的翻译值
    
     。这些示例中的绑定将导致在运行时从
    
     与应用程序的 CurrentUICulture 匹配的 .resx 文件中
    
     提取属性的值。
    
    -->
    
     
    
    <!--TitlePanel 包含应用程序的名称和页标题-->
    
    <StackPanel x:Name="TitlePanel" Grid.Row="0" Margin="12,17,0,28">
    
    <TextBlock x:Name="ApplicationTitle" Text="NORTHWIND TRADERS" Style="{StaticResource PhoneTextNormalStyle}"/>
    
    <TextBlock x:Name="PageTitle" Text="Customers" Margin="9,-7,0,0" Style="{StaticResource PhoneTextTitle1Style}"/>
    
    </StackPanel>
    
     
    
     
    
    <!--ContentPanel 包含 LongListSelector 和 LongListSelector ItemTemplate。在此处放置其他内容-->
    
    <Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
    
    <ListBox x:Name="MainLongListSelector" Margin="0,0,-12,0" ItemsSource="{Binding Customers}" SelectionChanged="MainLongListSelector_SelectionChanged">
    
    <ListBox.ItemTemplate>
    
    <DataTemplate>
    
    <StackPanel Margin="0,0,0,17" Width="432" Height="100">
    
    <TextBlock Text="{Binding CompanyName}" TextWrapping="NoWrap" Style="{StaticResource PhoneTextExtraLargeStyle}"/>
    
    <TextBlock Text="{Binding ContactName}" TextWrapping="NoWrap" Margin="12,-6,12,0" Style="{StaticResource PhoneTextSubtleStyle}"/>
    
    <TextBlock Text="{Binding Phone}" TextWrapping="NoWrap" Margin="12,-6,12,0" Style="{StaticResource PhoneTextSubtleStyle}"/>
    
    </StackPanel>
    
    </DataTemplate>
    
    </ListBox.ItemTemplate>
    
    </ListBox>
    
    </Grid>
    
     
    
     
    
    <!--取消注释,以显示对齐网格,从而帮助确保
    
     控件在公用边界上对齐。图像在系统栏中显示时的
    
     上边距为 -32px。如果隐藏了系统栏,则将此值设为 0
    
     (或完全删除边距)。
    
     
    
    在发送之前删除此 XAML 和图像本身。-->
    
    <!--<Image Source="/Assets/AlignmentGrid.png" VerticalAlignment="Top" Height="800" Width="480" Margin="0,-32,0,0" Grid.Row="0" Grid.RowSpan="2" IsHitTestVisible="False" />-->
    
    </Grid>
    
     
    
    </phone:PhoneApplicationPage> 

    更新MainPage.xaml.cs文件

    更新此方法

     

    View Code
    // 处理在 LongListSelector 中更改的选定内容
    
    private void MainLongListSelector_SelectionChanged(object sender, SelectionChangedEventArgs e) 
    
     { 
    
    // If selected item is null (no selection) do nothing
    
    if (MainLongListSelector.SelectedItem == null) 
    
    return; 
    
     
    
    // Navigate to the new page
    
     NavigationService.Navigate(new Uri("/DetailsPage.xaml?selectedItem=" + MainLongListSelector.SelectedIndex, UriKind.Relative)); 
    
     
    
    // Reset selected item to null (no selection)
    
     MainLongListSelector.SelectedItem = null; 
    
     } 

    在此我们修改对 NavigationService.Navigate 的调用,以使用选定项的索引而不使用 ItemViewModel 的 ID,因为在此示例中我们没有使用 ItemViewModel 类。

    更新DetailsPage.xaml文件

    View Code
    <phone:PhoneApplicationPage
    
     x:Class="MVVMODataTestApp.DetailsPage"
    
     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    
     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    
     xmlns:phone="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone"
    
     xmlns:shell="clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone"
    
     xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    
     xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    
     mc:Ignorable="d"
    
     FontFamily="{StaticResource PhoneFontFamilyNormal}"
    
     FontSize="{StaticResource PhoneFontSizeNormal}"
    
     Foreground="{StaticResource PhoneForegroundBrush}"
    
     SupportedOrientations="Portrait"  Orientation="Portrait"
    
     shell:SystemTray.IsVisible="True">
    
     
    
    <!--数据上下文设置为上面的示例数据和下面的示例数据集合中的第一项,并且 LayoutRoot 包含根网格,其他所有页面内容都位于根网格中-->
    
    <Grid x:Name="LayoutRoot" Background="Transparent">
    
    <Grid.RowDefinitions>
    
    <RowDefinition Height="Auto"/>
    
    <RowDefinition Height="*"/>
    
    </Grid.RowDefinitions>
    
    <StackPanel x:Name="TitlePanel" Grid.Row="0" Margin="12,17,0,28">
    
    <TextBlock x:Name="PageTitle" Text="NORTHWIND TRADERS"
    
     Style="{StaticResource PhoneTextNormalStyle}"/>
    
    <TextBlock x:Name="ListTitle" Text="{Binding CompanyName}" Margin="9,-7,0,0"
    
     Style="{StaticResource PhoneTextTitle1Style}" />
    
    </StackPanel>
    
    <ScrollViewer Grid.Row="1">
    
    <Grid x:Name="ContentPanel" Margin="12,0,12,0">
    
    <Grid.RowDefinitions>
    
    <RowDefinition Height="Auto" />
    
    <RowDefinition Height="Auto" />
    
    <RowDefinition Height="Auto" />
    
    <RowDefinition Height="Auto" />
    
    <RowDefinition Height="Auto" />
    
    <RowDefinition Height="Auto" />
    
    <RowDefinition Height="Auto" />
    
    </Grid.RowDefinitions>
    
    <Grid.ColumnDefinitions>
    
    <ColumnDefinition Width="165"/>
    
    <ColumnDefinition Width="*"/>
    
    </Grid.ColumnDefinitions>
    
    <TextBlock Text="Contact name:" Style="{StaticResource PhoneTextNormalStyle}"
    
     VerticalAlignment="Center" Grid.Row="0" Grid.Column="0"/>
    
    <TextBlock Text="Contact title:" Style="{StaticResource PhoneTextNormalStyle}"
    
     VerticalAlignment="Center" Grid.Row="1" Grid.Column="0"/>
    
    <TextBlock Text="Phone number:" Style="{StaticResource PhoneTextNormalStyle}"
    
     VerticalAlignment="Center" Grid.Row="2" Grid.Column="0"/>
    
    <TextBlock Text="Address:" Style="{StaticResource PhoneTextNormalStyle}"
    
     VerticalAlignment="Center" Grid.Row="3" Grid.Column="0"/>
    
    <TextBlock Text="City:" Style="{StaticResource PhoneTextNormalStyle}"
    
     VerticalAlignment="Center" Grid.Row="4" Grid.Column="0"/>
    
    <TextBlock Text="Region:" Style="{StaticResource PhoneTextNormalStyle}"
    
     VerticalAlignment="Center" Grid.Row="5" Grid.Column="0"/>
    
    <TextBlock Text="Postal code:" Style="{StaticResource PhoneTextNormalStyle}"
    
     VerticalAlignment="Center" Grid.Row="6" Grid.Column="0"/>
    
    <TextBox Text="{Binding ContactName, Mode=TwoWay}" Grid.Row="0" Grid.Column="1"/>
    
    <TextBox Text="{Binding ContactTitle, Mode=TwoWay}" Grid.Row="1" Grid.Column="1"/>
    
    <TextBox Text="{Binding Phone, Mode=TwoWay}" Grid.Row="2" Grid.Column="1"/>
    
    <TextBox Text="{Binding Address, Mode=TwoWay}" Grid.Row="3" Grid.Column="1"/>
    
    <TextBox Text="{Binding City, Mode=TwoWay}" Grid.Row="4" Grid.Column="1"/>
    
    <TextBox Text="{Binding Region, Mode=TwoWay}" Grid.Row="5" Grid.Column="1"/>
    
    <TextBox Text="{Binding PostalCode, Mode=TwoWay}" Grid.Row="6" Grid.Column="1"/>
    
    </Grid>
    
    </ScrollViewer>
    
    </Grid>
    
     
    
     
    
    </phone:PhoneApplicationPage> 
    
     

    该代码包含绑定到 Customer 对象(通过 MainViewModel 选定)属性的 TextBox 控件(视图)。访问读/写版本的 Northwind 数据服务时,该绑定可进行双向启用更新。

    更新DetailsPage.xaml.cs文件

    更新方法

     

    View Code
    // 导航页面以将数据上下文设置为列表中的所选项时
    
    protected override void OnNavigatedTo(NavigationEventArgs e) 
    
     { 
    
    if (DataContext == null) 
    
     { 
    
    string selectedIndex = ""; 
    
    if (NavigationContext.QueryString.TryGetValue("selectedItem", out selectedIndex)) 
    
     { 
    
    int index = int.Parse(selectedIndex); 
    
     DataContext = App.ViewModel.Customers[index]; 
    
     } 
    
     } 
    
     } 

    这会将视图的绑定上下文设置为当前选定的 Customer(通过 MainViewModel 公开)。

    维护页面状态

    在此过程中,您需要添加用来维护页面状态的代码。用户停止在应用中导航时,应用通常进入休眠状态。在休眠状态下,应用会保存在内存中,因此,当用户返回到应用时,应用几乎可以立即恢复。这种快速应用切换是自动启用的。但是,在休眠状态下可以终止应用。设计应用时使它可以处理这些状态变更很重要。有关更多信息,请参见 Windows Phone 的应用激活和取消激活

    Windows Phone 的 OData 客户端包含用于帮助管理这些状态转换的 DataServiceState 类。ViewModel 还必须公开可允许应用访问此项功能的方法,以维护 ViewModel 中数据的状态。

    在 MainViewModel 类中添加以下 SaveState ,RestoreState方法:

     

    View Code
    // Return a string serialization of the application state.
    
    public string SaveState() 
    
     { 
    
    if (App.ViewModel.IsDataLoaded) 
    
     { 
    
    // Create a new dictionary to store binding collections. 
    
    var collections = new Dictionary<string, object>(); 
    
     
    
    // Add the current Customers binding collection.
    
     collections["Customers"] = Customers; 
    
     
    
    // Return the serialized context and binding collections.
    
    return DataServiceState.Serialize(_context, collections); 
    
     } 
    
    else
    
     { 
    
    return string.Empty; 
    
     } 
    
     } 
    
     
    
    // Restores the view model state from the supplied state serialization.
    
    public void RestoreState(string appState) 
    
     { 
    
    // Create a dictionary to hold any stored binding collections.
    
    Dictionary<string, object> collections; 
    
     
    
    if (!string.IsNullOrEmpty(appState)) 
    
     { 
    
    // Deserialize the DataServiceState object.
    
    DataServiceState state 
    
     = DataServiceState.Deserialize(appState); 
    
     
    
    // Restore the context and binding collections.
    
    var context = state.Context as NorthwindEntities; 
    
     collections = state.RootCollections; 
    
     
    
    // Get the binding collection of Customer objects.
    
    DataServiceCollection<Customer> customers 
    
     = collections["Customers"] as DataServiceCollection<Customer>; 
    
     
    
    // Initialize the application with stored data. 
    
    App.ViewModel.LoadData(context, customers); 
    
     } 
    
     } 

    更新App.xaml.cs以下方法

     

    View Code
    // 停用应用程序(发送到后台)时执行的代码
    
    // 此代码在应用程序关闭时不执行
    
    private void Application_Deactivated(object sender, DeactivatedEventArgs e) 
    
     { 
    
    // 确保所需的应用程序状态在此处保持不变。
    
    if (App.ViewModel.IsDataLoaded) 
    
     { 
    
    // Store application state in the state dictionary. 
    
    PhoneApplicationService.Current.State["ApplicationState"] 
    
     = ViewModel.SaveState(); 
    
     } 
    
     } 

    此代码调用 ViewModel 中的 SaveState 方法,之后返回一个表示序列化 DataServiceState 对象的字符串。

     

    View Code
    // 激活应用程序(置于前台)时执行的代码
    
    // 此代码在首次启动应用程序时不执行
    
    private void Application_Activated(object sender, ActivatedEventArgs e) 
    
     { 
    
    // If data is not still loaded, try to get it from the state store.
    
    if (!ViewModel.IsDataLoaded) 
    
     { 
    
    if (PhoneApplicationService.Current.State.ContainsKey("ApplicationState")) 
    
     { 
    
    // Get back the serialized data service state from the dictionary.
    
    string appState = 
    
    PhoneApplicationService.Current.State["ApplicationState"] 
    
    as string; 
    
     
    
    // Use the returned dictionary to restore the state of the data service.
    
    App.ViewModel.RestoreState(appState); 
    
     } 
    
    else
    
     { 
    
    // Load the data since it is not persisted in the state dictionary. 
    
    App.ViewModel.LoadData(); 
    
     } 
    
     } 
    
     } 

    此代码调用 ViewModel 中的 RestoreState 方法,并返回状态字典中序列化的 DataServiceState 对象。如果状态字典中没有此字符串,则会从数据服务加载数据。

    添加应用栏

    添加现有项(路径:C:\Program Files (x86)\Microsoft SDKs\Windows Phone\v8.0\Icons\Dark),分别选择save.png,refresh.png,cancel.png

    然后分别选择修改属性:

    在MainViewModel.cs中提供方法实现

     

    View Code
    public void CancelCustomersAsyncLoad() 
    
     { 
    
    // Call the CancelAsyncLoad method on the binding collection.
    
    this.Customers.CancelAsyncLoad(); 
    
     } 
    
    public void SaveChanges() 
    
     { 
    
    // Start the save changes operation. 
    
    this._context.BeginSaveChanges(OnChangesSaved, this._context); 
    
     } 
    
     
    
    private void OnChangesSaved(IAsyncResult result) 
    
     { 
    
    // Use the Dispatcher to ensure that the 
    
    // asynchronous call returns in the correct thread.
    
    Deployment.Current.Dispatcher.BeginInvoke(() => 
    
     { 
    
    this._context = result.AsyncState as NorthwindEntities; 
    
     
    
    try
    
     { 
    
    // Complete the save changes operation.
    
    this._context.EndSaveChanges(result); 
    
     } 
    
    catch (DataServiceRequestException ex) 
    
     { 
    
    // Ideally, we should not create a UI element 
    
    // from the ViewModel. A better way is to use a 
    
    // service or event to report exceptions to the view.
    
    MessageBox.Show(string.Format( 
    
    "{0} The target Northwind data service ('{1}') is read-only.", 
    
     ex.Message, this._context.BaseUri)); 
    
     } 
    
     }); 
    
     } 
    
     
    
    public void Refresh() 
    
     { 
    
    // Cache the current merge option and change 
    
    // it to MergeOption.OverwriteChanges.
    
    MergeOption cachedOption = _context.MergeOption; 
    
     _context.MergeOption = MergeOption.OverwriteChanges; 
    
     
    
    // Reload data from the data service.
    
    this.LoadData(); 
    
     
    
    // Reset the merge option.
    
     _context.MergeOption = cachedOption; 
    
     } 

    在MainPage.xaml.cs中添加方法

     

    View Code
    private void AppBarCancel_Click(object sender, EventArgs e) 
    
     { 
    
    App.ViewModel.CancelCustomersAsyncLoad(); 
    
     } 
    
    private void AppBarRefresh_Click(object sender, EventArgs e) 
    
     { 
    
    // Reload the data from the OData service.
    
    App.ViewModel.Refresh(); 
    
     } 

    在DetailsPage.xaml.cs中添加方法

     

    View Code
    private void AppBarSave_Click(object sender, EventArgs e) 
    
     { 
    
    App.ViewModel.SaveChanges(); 
    
     } 

    在MainPage.xaml中插入按钮

     

    View Code
    <phone:PhoneApplicationPage.ApplicationBar>
    
    <shell:ApplicationBar IsVisible="True" IsMenuEnabled="True" >
    
    <shell:ApplicationBarIconButton IconUri="AppBarRefresh.png"
    
     Text="Refresh" Click="AppBarRefresh_Click" />
    
    <shell:ApplicationBarIconButton IconUri="AppBarCancel.png"
    
     Text="Cancel" Click="AppBarCancel_Click" />
    
    </shell:ApplicationBar>
    
    </phone:PhoneApplicationPage.ApplicationBar> 

    在DetailsPage.xaml中插入按钮

     

    View Code
    <phone:PhoneApplicationPage.ApplicationBar>
    
    <shell:ApplicationBar IsVisible="True" IsMenuEnabled="True" >
    
    <shell:ApplicationBarIconButton IconUri="AppBarSave.png"
    
     Text="Save" Click="AppBarSave_Click" />
    
    </shell:ApplicationBar>
    
    </phone:PhoneApplicationPage.ApplicationBar> 

    效果:

    MainPage.xaml

    DetailsPage.xaml

    运行:

    发现,没有数据出现,打开里面请求的路径:http://services.odata.org/Northwind/Northwind.svc/Customers()

    后来不知道为什么又行了,晕~

    Windows Phone 的 OData 客户端 http://msdn.microsoft.com/zh-cn/library/gg521146(v=vs.105).aspx

    注意:

    当使用本地的wcf data service 服务的时候,要使用ip或者机器名来替代localhost,否则是请求不到数据的。

    http://msdn.microsoft.com/zh-cn/library/windowsphone/develop/jj684580(v=vs.105).aspx

    作者:王春明 出处:http://wangchunming.cnblogs.com/ 本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
  • 相关阅读:
    SSL 数据加密原理简述
    MQTT 协议 部分细节
    ARM汇编--汇编中符号和变量
    Kconfig 配置文件编码规则
    ARM汇编指令-STMFD/LDMFD
    python类属性和对象属性、类的普通方法和静态方法
    ARM汇编---程序获取符号的物理地址
    Spring源码分析:非懒加载的单例Bean初始化前后的一些操作
    Spring源码分析:非懒加载的单例Bean初始化过程(下)
    Spring源码分析:非懒加载的单例Bean初始化过程(上)
  • 原文地址:https://www.cnblogs.com/wangchunming/p/3035509.html
Copyright © 2011-2022 走看看