使用 Rx.NET 构建 Windows Phone 天气应用的完整指南
1. 创建 Windows Phone 项目
要让天气应用正常运行,需先创建新项目、导入所有库并创建必要的服务引用,具体步骤如下:
1. 启动 Visual Studio 2010 Express for Windows Phone,创建一个新的 Windows Phone 应用程序项目,命名为 WeatherRx。
2. 在 MainPage.xaml 中,将应用程序名称改为 WeatherRx,页面标题改为 Weather App(当然,你也可按喜好命名)。
3. 由于使用 Rx.NET 构建此应用,在解决方案资源管理器中右键单击项目名称,选择“添加引用”,添加对 Microsoft.Phone.Reactive 和 System.Observable 程序集的引用。
4. 要添加对天气服务的引用,右键单击项目名称,选择“添加服务引用”。在弹出的对话框中,在地址文本框输入:http://www.webservicex.net/globalweather.asmx,然后点击“转到”按钮。
5. 左侧应出现 GlobalWeather 服务,点击旁边的箭头,确保选择 GlobalWeatherSoap 服务,然后将命名空间重命名为 svcWeather。
6. 最终的“添加服务引用”屏幕应如图 18 - 9 所示。
7. 点击“确定”按钮。
2. 创建用户界面
应用的目标是创建一个类似图 18 - 10 的屏幕。为实现此目标,以下是页面标题之后的可视化元素的 XAML 代码,你也可从本章下载部分的示例代码中复制粘贴:
<!--ContentPanel - place additional content here-->
<Grid x:Name="ContentGrid" Grid.Row="1">
<TextBox Height="72" HorizontalAlignment="Left" Margin="0,51,0,0" Name="txtCityName"
Text="" VerticalAlignment="Top" Width="480" />
<TextBlock Height="53" HorizontalAlignment="Left" Margin="6,13,0,0" Name="lblLegend"
Text="Enter U.S. City Name below for current Weather" VerticalAlignment="Top"
Width="462" />
<TextBlock Height="30" HorizontalAlignment="Left" Margin="6,129,0,0"
Name="lblTemperature" Text="Current Temperature" VerticalAlignment="Top"
Width="435" />
<Image Height="150" HorizontalAlignment="Left" Margin="241,213,0,0"
Name="imgWeather" Stretch="Fill" VerticalAlignment="Top" Width="200" />
<TextBlock Height="30" HorizontalAlignment="Left" Margin="6,162,0,0" Name="lblWind"
Text="Current Wind Conditions" VerticalAlignment="Top" Width="435" />
<TextBlock Height="30" Margin="6,379,39,0" Name="lblStatus" Text=""
VerticalAlignment="Top" />
<Button Content="Retry" Height="72" HorizontalAlignment="Left" Margin="69,429,0,0"
Name="btnRetry" VerticalAlignment="Top" Width="160" Visibility="Collapsed"
Click="btnRetry_Click" />
<Button Content="Quit" Height="72" HorizontalAlignment="Right" Margin="0,429,79,0"
Name="btnQuit" VerticalAlignment="Top" Width="160" Visibility="Collapsed"
Click="btnQuit_Click" />
</Grid>
</Grid>
注意,最后一个
</Grid>
语句关闭的是未在上述片段中显示的 LayoutGrid 元素。
3. 添加获取天气信息的逻辑
设计元素和正确的引用就位后,就可向应用添加代码。此示例将代码拆分为多个函数以提高可读性:
1. 右键单击项目名称,选择“添加引用”,从列表中选择 System.Xml.Linq 程序集。你需要几个 LINQ - to - XML 函数来解析 Web 服务返回的字符串。
2. 点击 MainPage.xaml 并选择“查看代码”打开 MainPage.xaml.cs,在页面顶部添加以下
using
语句:
using Microsoft.Phone.Reactive;
using System.Xml.Linq;
-
在
MainPage()构造函数上方添加以下模块级变量声明:
svcWeather.GlobalWeatherSoapClient weatherClient = new svcWeather.GlobalWeatherSoapClient();
IObservable<IEvent<GetWeatherCompletedEventArgs>> _weather;
const string conCountry = "United States";
- 注意,这里指定常量“United States”作为 Web 服务的第二个参数。你也可通过使国家选择动态化来增强应用。
-
在
MainPage()构造函数的InitializeComponent()语句之后添加以下代码:
WireUpWeatherEvents();
WireUpKeyEvents();
- 这里将 Web 服务和按键事件分别在不同函数中连接,此技术在处理错误恢复时非常有用。
-
创建
WireUpWeatherEvents函数及其支持的GetWeatherSubject函数,粘贴以下代码。注意如何创建一个单独的函数(GetWeatherSubject)从天气 Web 服务事件返回一个可观察集合:
private void WireUpWeatherEvents()
{
GetWeatherSubject();
_weather.ObserveOn(Deployment.Current.Dispatcher)
.Subscribe(evt =>
{
if (evt.EventArgs.Result!= null)
{
string strXMLResult = evt.EventArgs.Result;
XElement weatherElements = XElement.Parse(strXMLResult);
string strTemperature = weatherElements.Element("Temperature").Value;
string strWind = weatherElements.Element("Wind").Value;
lblTemperature.Text = "Current Temperature: " + strTemperature;
lblWind.Text = "Current Wind: " + strWind;
}
}
);
}
private void GetWeatherSubject()
{
if (_weather == null)
{
_weather = Observable.FromEvent<svcWeather.GetWeatherCompletedEventArgs>(weatherClient, "GetWeatherCompleted");
}
}
-
创建
WireUpKeyEvents函数,该函数从 KeyUp 事件定义一个可观察集合,并通过添加以下代码创建对该集合的订阅:
private void WireUpKeyEvents()
{
var keys = Observable.FromEvent<KeyEventArgs>(txtCityName, "KeyUp").Throttle(TimeSpan.FromSeconds(1)).DistinctUntilChanged();
keys.ObserveOn(Deployment.Current.Dispatcher).Subscribe(evt =>
{
if (txtCityName.Text.Length >= 5)
{
WireUpWeatherEvents();
weatherClient.GetWeatherAsync(txtCityName.Text, conCountry);
}
});
}
- 按 F5 运行应用。你会看到一个屏幕提示你输入美国城市名称以获取当前天气。输入城市名称后,你应能得到当前天气和风力状况的合理估计。图 18 - 11 显示了纽约地区的示例输出。
4. Rx.NET 工具剖析
构建此应用使用了 Rx.NET 从天气 Web 服务调用的异步响应创建可观察集合,使用以下语句创建该集合:
_weather = Observable.FromEvent<svcWeather.GetWeatherCompletedEventArgs>(weatherClient, "GetWeatherCompleted");
然后为该数据源定义一个观察者,当数据从 Web 服务推送到观察者时,通过在 UI 中显示数据来采取行动。
接下来,为 txtCityName 文本框中的 KeyUp 事件创建一个可观察集合,并为该集合创建一个观察者。因此,每当用户暂停输入一秒钟时,键数据源上的观察者会验证城市名称字段中是否输入了五个或更多字母,然后调用
GetWeatherAsync
函数,该函数又会向天气 Web 服务发出异步请求。
所有这些调用的异步性质很重要,如果应用中还有其他功能,可在所有异步请求完成时继续使用。如前文所述,异步处理是 Rx.NET 专门设计用于解决的领域。在使用 Rx.NET 之前进行异步编程时,.NET 提供了两种方法来实现异步方法设计模式,第一个方法启动计算,第二个方法获取计算结果。如果有多个异步操作,即使是像天气示例中那样简单的操作,管理这些多个方法也会很快成为难题。而 Rx.NET 还尝试在所有可用核心上并行化异步请求,这为已经很强大的清晰性和强大的观察者查询功能又增添了巨大优势。
5. Rx.NET 中的错误处理
在异步编程领域,尤其是分布式异步编程中,错误是不可避免的。Rx.NET 观察者提供了一个单独的
OnError
事件处理程序来处理可能出现的任何意外错误。例如,为使 WeatherRx 应用更健壮,在
weather.Subscribe
调用中添加一个
OnError
处理程序,结果代码如下:
_weather.ObserveOn(Deployment.Current.Dispatcher)
.Subscribe(evt =>
{
if (evt.EventArgs.Result!= null)
{
string strXMLResult = evt.EventArgs.Result;
XElement weatherElements = XElement.Parse(strXMLResult);
string strTemperature = weatherElements.Element("Temperature").Value;
string strWind = weatherElements.Element("Wind").Value;
lblTemperature.Text = "Current Temperature: " + strTemperature;
lblWind.Text = "Current Wind: " + strWind;
}
},
ex =>
{
Deployment.Current.Dispatcher.BeginInvoke(() => lblStatus.Text = ex.Message);
}
);
注意
Deployment.Current.Dispatcher.BeginInvoke
语句的使用,它用于解决前面讨论的跨线程访问问题(这是一个 lambda 表达式,并且在其自身主体中使用了 lambda 表达式)。在上述代码中,
OnError
处理程序只是显示异常文本,但你可以深入剖析错误并提供可能的纠正措施。例如,如果 Web 服务在指定地址不可用,你可以重试调用到 Web 服务的不同位置。Rx.NET 还有异常处理操作符
Catch
、
Finally
、
OnErrorResumeNext
和
Retry
,有助于从错误中恢复。
6. 使用 Rx.NET 处理数据连接问题
在手机上,数据连接缓慢或丢失是常见问题。理想情况下,手机应用应检测此类问题并提供恢复机制。处理手机上缓慢或丢失连接的两种潜在方法是让用户决定应用是否应重试连接超时或丢失之前正在做的事情,以及提供自动重试机制。
Rx.NET 在这两种情况下都能提供帮助。此外,Rx.NET 包括一个特殊的
Timeout
操作,如果在用户指定的时间间隔内未从其可观察对象接收到数据(如 Web 服务回调),则会生成超时错误。让我们看看
Timeout
操作的实际应用,将
WireUpWeatherEvents
函数修改为如果两秒内未获取任何数据则超时:
1. 将 WeatherRx 应用的
WireUpEvents()
函数替换为以下代码:
private void WireUpWeatherEvents()
{
GetWeatherSubject();
_weather.ObserveOn(Deployment.Current.Dispatcher)
.Timeout(TimeSpan.FromSeconds(2))
.Subscribe(evt =>
{
if (evt.EventArgs.Result!= null)
{
string strXMLResult = evt.EventArgs.Result;
XElement weatherElements = XElement.Parse(strXMLResult);
string strTemperature = weatherElements.Element("Temperature").Value;
string strWind = weatherElements.Element("Wind").Value;
lblTemperature.Text = "Current Temperature: " + strTemperature;
lblWind.Text = "Current Wind: " + strWind;
}
},
ex =>
{
Deployment.Current.Dispatcher.BeginInvoke(() => lblStatus.Text = ex.Message);
}
);
}
运行应用,会发现两秒后应用立即超时,并在模拟器上显示超时异常文本。这是因为在应用启动时立即订阅了 Web 服务事件,而在应用启动两秒后未获取任何数据,该订阅就超时了。代码需要进行一些重构,应在调用 Web 服务之前订阅其事件,并且要确保只创建一次此订阅。
2. 从
MainPage
构造函数中移除对
WireUpWeatherEvents
的调用,并将其放在
WireUpKeyEvents
函数中,如下所示:
private void WireUpKeyEvents()
{
var keys = Observable.FromEvent<KeyEventArgs>(txtCityName, "KeyUp").Throttle(TimeSpan.FromSeconds(1)).DistinctUntilChanged();
keys.ObserveOn(Deployment.Current.Dispatcher).Subscribe(evt =>
{
if (txtCityName.Text.Length >= 5)
{
WireUpWeatherEvents();
weatherClient.GetWeatherAsync(txtCityName.Text, conCountry);
}
});
}
现在超时功能应能正常工作。不过,从天气服务返回有效响应可能需要略多于两秒的时间(实际上,由于该服务的负载,你可能需要将超时值提高到 300 秒)。
Rx.NET 还提供了一个
Retry
方法,可选择传入一个参数指定重试订阅可观察集合的次数。如果不指定该参数,Rx.NET 会无限重试订阅可观察集合。处理连接缺失或缓慢的一种方法是重试订阅两到三次,如果仍不成功,给用户提供再次重试或取消的选项。
7. 修改 WeatherRx 以管理缓慢的数据连接
要修改 WeatherRx 应用,首先在 UI 中添加按钮,让用户可以重试失败的连接或优雅退出,然后在应用中添加代码以响应这些新用户界面元素上的事件。
1. 打开 MainPage.xaml,在
lblStatus
文本块下方添加两个按钮,如图 18 - 12 所示。将第一个按钮命名为
btnRetry
,将其
Content
属性设置为“Retry”;将第二个按钮命名为
btnQuit
,将其
Content
属性设置为“Quit”。将两个按钮的
Visibility
设置为
Collapsed
。
2. 双击“Retry”按钮,在
btnRetry_Click
函数中添加以下处理程序代码:
private void btnRetry_Click(object sender, RoutedEventArgs e)
{
btnQuit.Visibility = System.Windows.Visibility.Collapsed;
btnRetry.Visibility = System.Windows.Visibility.Collapsed;
lblStatus.Text = "";
WireUpWeatherEvents();
weatherClient.GetWeatherAsync(txtCityName.Text, conCountry);
}
-
双击“Quit”按钮,在
btnQuit_Click函数中添加以下代码:
private void btnQuit_Click(object sender, RoutedEventArgs e)
{
btnQuit.Visibility = System.Windows.Visibility.Collapsed;
btnRetry.Visibility = System.Windows.Visibility.Collapsed;
lblStatus.Text = "";
}
-
最后,确保在任何给定时间对天气 Web 服务只有一个订阅。将
WireUpWeatherEvents方法修改如下,注意现在将超时值设置为更合理的 30 秒:
private void WireUpWeatherEvents()
{
GetWeatherSubject();
_weather.ObserveOn(Deployment.Current.Dispatcher)
.Timeout(TimeSpan.FromSeconds(30))
.Subscribe(evt =>
{
if (evt.EventArgs.Result!= null)
{
string strXMLResult = evt.EventArgs.Result;
XElement weatherElements = XElement.Parse(strXMLResult);
string strTemperature = weatherElements.Element("Temperature").Value;
string strWind = weatherElements.Element("Wind").Value;
lblTemperature.Text = "Current Temperature: " + strTemperature;
lblWind.Text = "Current Wind: " + strWind;
}
},
ex =>
{
Deployment.Current.Dispatcher.BeginInvoke(() => lblStatus.Text = ex.Message);
Deployment.Current.Dispatcher.BeginInvoke(() => btnQuit.Visibility = System.Windows.Visibility.Visible);
Deployment.Current.Dispatcher.BeginInvoke(() => btnRetry.Visibility = System.Windows.Visibility.Visible);
}
);
}
此示例展示了一种处理 Windows Phone 设备上连接问题的方法,即指定一个超时时间,如果在该时间内未得到响应,提示用户重试或退出。
8. 使用 Rx.NET 处理多个并发请求
目前创建的天气应用会根据用户输入的城市名称发送多个天气数据请求,但天气 Web 服务返回数据的顺序不保证。例如,用户先输入纽约,然后输入波士顿,纽约市的天气结果可能在波士顿之后返回,但用户看到屏幕上的搜索词是波士顿,却可能未意识到看到的是纽约的天气。如果应用能取消最新请求之前的所有天气请求就好了,例如,一旦发出波士顿天气请求,纽约天气请求就会被取消。
Rx.NET 提供了这样的解决方案,其中的
TakeUntil()
和
Switch
操作符允许取消在最新操作之前发生且仍在进行中的操作。通过使用优雅的 LINQ 查询,这些操作符将可观察集合关联起来。但目前在 Windows Phone 上的 .NET Framework 当前实现中,无法将异步 SOAP Web 服务调用的开始与结束关联起来,问题根源在于 Windows Phone 上的 Windows Communication Foundation 库中排除了
CreateChannel
方法的实现,这是微软为优化 Windows Phone 上的 .NET Framework 所做的调整。
不过,取消进行中请求的技术仍适用于安装了完整 .NET Framework 的客户端(Windows Forms 和 WPF 应用程序)以及 Silverlight 平台。也许在不久的将来,此技术也将在 Windows Phone 上可用,因此了解其基础知识很有用。
对于天气应用,通过每次用户输入新城市名称时为天气服务创建一个新的可观察集合来模拟取消这些请求的技术。但要注意,创建的可观察订阅会监听任何已完成的天气服务请求,而不是特定的请求。换句话说,由于 Windows Phone 框架当前实现的上述限制,目前在 Windows Phone 上取消进行中请求的实现不完整且不可靠,目前无法将 SOAP Web 服务调用的开始与结束关联起来。
要使在可观察集合上的操作在进行中时能够取消,需要修改代码,使可观察集合能够进行 LINQ 查询。按以下步骤实现操作取消:
1. 在
MainPage
类的顶部(构造函数上方)粘贴以下代码,声明一个模块级的可观察集合,用于城市名称文本框的
KeyUp
事件:
IObservable<IEvent<KeyEventArgs>> _keys;
-
通过在代码中添加以下两个方法,公开城市名称文本框的
KeyUp事件和 Web 服务回调的可观察对象:
private IObservable<IEvent<GetWeatherCompletedEventArgs>> GetWeatherSubject()
{
return Observable.FromEvent<svcWeather.GetWeatherCompletedEventArgs>(weatherClient, "GetWeatherCompleted");
}
private void GetKeys()
{
if (_keys == null)
{
_keys = Observable.FromEvent<KeyEventArgs>(txtCityName, "KeyUp").Throttle(TimeSpan.FromSeconds(1)).DistinctUntilChanged();
}
}
-
使取消操作生效的关键代码如下,特别注意 LINQ 查询,它建立了
KeyUp事件的可观察集合与 Web 服务回调的可观察集合之间的关系。如果 Windows Phone 框架支持所谓的 Web 服务调用的异步模式(使用BeginXXX/EndXXX方法),就可以建立键序列和 Web 服务调用之间的直接关系,但以下代码中两者之间只是松散或间接的关系,因为每个订阅都会监听天气 Web 服务的所有响应,而不仅仅是特定的响应。在 LINQ 语句之后,Switch()操作符指示应用一旦keys可观察集合中有新的键序列等待,就处理掉对天气 Web 服务的旧订阅:
private void WireUpWeatherEvents()
{
GetKeys();
var latestWeather = (from term in _keys
select GetWeatherSubject()
.Finally(() =>
{
Deployment.Current.Dispatcher.BeginInvoke(() => Debug.WriteLine("Disposed of prior subscription"));
})
).Switch();
latestWeather.ObserveOnDispatcher()
.Subscribe(evt =>
{
if (evt.EventArgs.Result!= null)
{
string strXMLResult = evt.EventArgs.Result;
XElement weatherElements = XElement.Parse(strXMLResult);
string strTemperature = weatherElements.Element("Temperature").Value;
string strWind = weatherElements.Element("Wind").Value;
lblTemperature.Text = "Current Temperature: " + strTemperature;
lblWind.Text = "Current Wind: " + strWind;
}
},
ex =>
{
Deployment.Current.Dispatcher.BeginInvoke(() => lblStatus.Text = ex.Message);
}
);
}
代码中的
.Finally
语句用于在一个可观察集合被移除并被新的集合替换时,在输出窗口中打印“Disposed of prior subscription”消息,这在
_keys
模块级可观察集合中有新事件发生时会出现。
综上所述,通过以上步骤,我们可以使用 Rx.NET 构建一个功能较为完善的 Windows Phone 天气应用,并且能够处理常见的问题,如错误处理、数据连接问题和并发请求问题。虽然在 Windows Phone 平台上存在一些限制,但 Rx.NET 提供的强大功能和灵活的操作符仍然为我们开发高质量的异步应用提供了很大的帮助。
使用 Rx.NET 构建 Windows Phone 天气应用的完整指南
9. 整体流程梳理
为了更清晰地理解整个应用的开发过程,我们可以梳理一下整体的流程,如下表所示:
|步骤|操作内容|
| ---- | ---- |
|创建项目|启动 Visual Studio 2010 Express for Windows Phone,创建名为 WeatherRx 的项目,修改应用名称和页面标题,添加必要的程序集引用和服务引用|
|创建界面|在 MainPage.xaml 中编写 XAML 代码,构建应用的用户界面|
|添加逻辑|在 MainPage.xaml.cs 中添加代码,包括变量声明、事件连接函数等,实现获取天气信息的功能|
|处理错误|在订阅中添加
OnError
处理程序,使用 Rx.NET 的异常处理操作符处理错误|
|处理连接问题|使用
Timeout
操作和
Retry
方法处理数据连接缓慢或丢失的问题|
|处理并发请求|通过修改代码,使用
TakeUntil()
和
Switch
操作符处理多个并发请求|
下面是这个流程的 mermaid 流程图:
graph LR
classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px
A([开始]):::startend --> B(创建项目):::process
B --> C(创建界面):::process
C --> D(添加逻辑):::process
D --> E(处理错误):::process
E --> F(处理连接问题):::process
F --> G(处理并发请求):::process
G --> H([结束]):::startend
10. 关键代码分析
在整个开发过程中,有一些关键代码起到了重要的作用,下面对这些代码进行详细分析。
10.1 可观察集合的创建
_weather = Observable.FromEvent<svcWeather.GetWeatherCompletedEventArgs>(weatherClient, "GetWeatherCompleted");
_keys = Observable.FromEvent<KeyEventArgs>(txtCityName, "KeyUp").Throttle(TimeSpan.FromSeconds(1)).DistinctUntilChanged();
-
Observable.FromEvent方法用于从事件创建可观察集合。_weather可观察集合监听天气服务的GetWeatherCompleted事件,当事件触发时,会推送数据。 -
_keys可观察集合监听文本框的KeyUp事件,使用Throttle方法设置用户输入间隔为 1 秒,使用DistinctUntilChanged方法确保只有当输入内容发生变化时才推送数据。
10.2 订阅和处理数据
_weather.ObserveOn(Deployment.Current.Dispatcher)
.Subscribe(evt =>
{
if (evt.EventArgs.Result!= null)
{
string strXMLResult = evt.EventArgs.Result;
XElement weatherElements = XElement.Parse(strXMLResult);
string strTemperature = weatherElements.Element("Temperature").Value;
string strWind = weatherElements.Element("Wind").Value;
lblTemperature.Text = "Current Temperature: " + strTemperature;
lblWind.Text = "Current Wind: " + strWind;
}
},
ex =>
{
Deployment.Current.Dispatcher.BeginInvoke(() => lblStatus.Text = ex.Message);
}
);
-
ObserveOn(Deployment.Current.Dispatcher)确保订阅的回调函数在 UI 线程上执行,避免跨线程访问问题。 -
Subscribe方法用于订阅可观察集合,第一个参数是数据处理函数,当有数据推送时,会解析 XML 数据并更新 UI 显示。第二个参数是错误处理函数,当出现错误时,会在 UI 上显示错误信息。
10.3 处理并发请求的 LINQ 查询
var latestWeather = (from term in _keys
select GetWeatherSubject()
.Finally(() =>
{
Deployment.Current.Dispatcher.BeginInvoke(() => Debug.WriteLine("Disposed of prior subscription"));
})
).Switch();
-
通过 LINQ 查询,从
_keys可观察集合中获取每个新的键输入,然后为每个输入创建一个新的天气服务可观察集合。 -
Finally方法在可观察集合完成或被处理时执行,用于打印日志信息。 -
Switch操作符确保在有新的键输入时,处理掉之前的订阅,只保留最新的订阅。
11. 总结与注意事项
通过上述步骤,我们使用 Rx.NET 成功构建了一个 Windows Phone 天气应用,并处理了错误、数据连接和并发请求等问题。在开发过程中,有以下几点需要注意:
1.
异步编程的优势
:Rx.NET 的异步编程模型可以让应用在处理多个任务时更加高效,避免阻塞主线程,提高用户体验。
2.
跨线程访问问题
:在更新 UI 时,需要使用
Deployment.Current.Dispatcher.BeginInvoke
方法确保代码在 UI 线程上执行,避免出现跨线程访问异常。
3.
Windows Phone 平台的限制
:由于 Windows Phone 上的 .NET Framework 实现的限制,在处理并发请求时,目前无法建立 SOAP Web 服务调用的开始与结束之间的直接关系,需要采用一些模拟的方法。
4.
超时和重试设置
:合理设置
Timeout
和
Retry
参数可以提高应用在网络不稳定情况下的健壮性,但需要根据实际情况进行调整。
总之,Rx.NET 为开发 Windows Phone 应用提供了强大的工具和灵活的操作符,通过合理运用这些功能,可以开发出高质量的异步应用。希望本文能帮助你更好地理解和使用 Rx.NET 进行应用开发。
超级会员免费看
1

被折叠的 条评论
为什么被折叠?



