跨平台文件I / O的第一个镜头
在一般情况下,您将使用DependencyService为您的Xamarin.Forms应用程序提供对文件I / O功能的访问。从之前对DependencyService的探索中可以了解到,您可以在Portable Class Library项目中的接口中定义所需的函数,而实现这些函数的代码则驻留在各个平台的不同类中。
本章开发的文件I / O函数将在第24章“页面导航”中的NoteTaker应用程序中得到很好的使用。对于文件I / O的第一个镜头,让我们使用一个更简单的解决方案,名为TextFileTryout ,它实现了几个用于处理文本文件的函数。让我们限制自己让这个程序在iOS和Android上运行,暂时忘记Windows平台。
使用DependencyService的第一步是在PCL中创建一个界面,定义您需要的所有方法。这是TextFileTryout项目中的这样一个名为IFileHelper的接口:
namespace TextFileTryout
{
public interface IFileHelper
{
bool Exists(string filename);
void WriteText(string filename, string text);
string ReadText(string filename);
IEnumerable<string> GetFiles();
void Delete(string filename);
}
}
该接口定义了用于确定文件是否存在,一次写入和读取整个文本文件,枚举应用程序创建的所有文件以及删除文件的函数。 在每个平台实现中,这些功能仅限于与应用程序关联的专用文件区域。
然后,您可以在每个平台中实现此接口。 这是iOS项目中的FileHelper类,包含using指令和所需的Dependency属性:
using System;
using System.Collections.Generic;
using System.IO;
using Xamarin.Forms;
[assembly: Dependency(typeof(TextFileTryout.iOS.FileHelper))]
namespace TextFileTryout.iOS
{
class FileHelper : IFileHelper
{
public bool Exists(string filename)
{
string filepath = GetFilePath(filename);
return File.Exists(filepath);
}
public void WriteText(string filename, string text)
{
string filepath = GetFilePath(filename);
File.WriteAllText(filepath, text);
}
public string ReadText(string filename)
{
string filepath = GetFilePath(filename);
return File.ReadAllText(filepath);
}
public IEnumerable<string> GetFiles()
{
return Directory.GetFiles(GetDocsPath());
}
public void Delete(string filename)
{
File.Delete(GetFilePath(filename));
}
// Private methods.
string GetFilePath(string filename)
{
return Path.Combine(GetDocsPath(), filename);
}
string GetDocsPath()
{
return Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
}
}
}
此类必须显式实现IFileHelper接口,并包含具有类名称的Dependency属性。这些允许Xamarin.Forms中的DependencyService类在平台项目中找到IFileHelper的这种实现。底部的两个私有方法允许程序使用Environment.GetFolderPath方法中可用的应用程序私有存储的目录来构造完全限定的文件名。
在Xamarin.iOS和Xamarin.Android中,Environment.GetFolderPath的实现获取应用程序本地存储的特定于平台的区域,尽管该方法为两个平台返回的目录名称非常不同。
因此,除了不同的命名空间名称之外,Android项目中的FileHelper类与iOS项目中的类完全相同。
iOS和Android版本的FileHelper使用File类中的静态快捷方法和Directory的简单静态方法来获取与应用程序一起存储的所有文件。但是,Windows 8.1和Windows Phone 8.1项目中的IFileHelper的实现无法使用File类中的快捷方法,因为它们不可用,并且UWP项目中的Environment.GetFolderPath方法不可用。
此外,为这些Windows平台编写的应用程序应该使用Windows运行时API中实现的文件I / O函数。由于Windows运行时中的文件I / O功能是异步的,因此它们不适合由IFileHelper接口建立的接口。出于这个原因,三个Windows项目中的FileHelper版本被迫离开关键方法未实现。这是UWP项目中的版本:
using System;
using System.Collections.Generic;
using Xamarin.Forms;
[assembly: Dependency(typeof(TextFileTryout.UWP.FileHelper))]
namespace TextFileTryout.UWP
{
class FileHelper : IFileHelper
{
public bool Exists(string filename)
{
return false;
}
public void WriteText(string filename, string text)
{
throw new NotImplementedException("Writing files is not implemented");
}
public string ReadText(string filename)
{
throw new NotImplementedException("Reading files is not implemented");
}
public IEnumerable<string> GetFiles()
{
return new string[0];
}
public void Delete(string filename)
{
}
}
}
除命名空间名称外,Windows 8.1和Windows Phone 8.1项目中的FileHelper版本相同。
通常,应用程序需要使用DependencyService.Get方法引用每个平台中的方法。 但是,TextFileTryout程序通过在PCL项目中定义一个名为FileHelper的类(也实现了IFileHelper)使事情变得容易,但是对DependencyService的Get方法的调用合并了这些方法的平台版本:
namespace TextFileTryout
{
class FileHelper : IFileHelper
{
IFileHelper fileHelper = DependencyService.Get<IFileHelper>();
public bool Exists(string filename)
{
return fileHelper.Exists(filename);
}
public void WriteText(string filename, string text)
{
fileHelper.WriteText(filename, text);
}
public string ReadText(string filename)
{
return fileHelper.ReadText(filename);
}
public IEnumerable<string> GetFiles()
{
IEnumerable<string> filepaths = fileHelper.GetFiles();
List<string> filenames = new List<string>();
foreach (string filepath in filepaths)
{
filenames.Add(Path.GetFileName(filepath));
}
return filenames;
}
public void Delete(string filename)
{
fileHelper.Delete(filename);
}
}
}
请注意,GetFiles方法对从平台实现返回的文件名执行一些小手术。 从GetFiles的平台实现获得的文件名是完全限定的,虽然看到iOS和Android用于应用程序本地存储的文件夹名称可能很有趣,但这些文件名将显示在ListView中,其中文件夹名称 只会是一个分心,所以这个GetFiles方法剥离文件路径。
TextFileTryoutPage类测试这些函数。 XAML文件包括文件名条目,文件内容编辑器,标有“保存”的按钮,以及包含所有以前保存的文件名的ListView:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="TextFileTryout.TextFileTryoutPage">
<ContentPage.Padding>
<OnPlatform x:TypeArguments="Thickness"
iOS="0, 20, 0, 0" />
</ContentPage.Padding>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Entry x:Name="filenameEntry"
Grid.Row="0"
Placeholder="filename" />
<Editor x:Name="fileEditor"
Grid.Row="1">
<Editor.BackgroundColor>
<OnPlatform x:TypeArguments="Color"
WinPhone="#D0D0D0" />
</Editor.BackgroundColor>
</Editor>
<Button x:Name="saveButton"
Text="Save"
Grid.Row="2"
HorizontalOptions="Center"
Clicked="OnSaveButtonClicked" />
<ListView x:Name="fileListView"
Grid.Row="3"
ItemSelected="OnFileListViewItemSelected">
<ListView.ItemTemplate>
<DataTemplate>
<TextCell Text="{Binding}">
<TextCell.ContextActions>
<MenuItem Text="Delete"
IsDestructive="True"
Clicked="OnDeleteMenuItemClicked" />
</TextCell.ContextActions>
</TextCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
</ContentPage>
为了简单起见,所有处理都在没有ViewModel的代码隐藏文件中执行。 代码隐藏文件实现了XAML文件中的所有事件处理程序。 “保存”按钮检查文件是否首先存在,如果存在则显示警告框。 选择ListView中的一个文件将其加载。此外,ListView实现了一个上下文菜单来删除文件。 所有文件I / O函数都是PCL中定义的FileHelper类的方法,并实例化为类顶部的字段:
public partial class TextFileTryoutPage : ContentPage
{
FileHelper fileHelper = new FileHelper();
public TextFileTryoutPage()
{
InitializeComponent();
RefreshListView();
}
async void OnSaveButtonClicked(object sender, EventArgs args)
{
string filename = filenameEntry.Text;
if (fileHelper.Exists(filename))
{
bool okResponse = await DisplayAlert("TextFileTryout",
"File " + filename +
" already exists. Replace it?",
"Yes", "No");
if (!okResponse)
return;
}
string errorMessage = null;
try
{
fileHelper.WriteText(filenameEntry.Text, fileEditor.Text);
}
catch (Exception exc)
{
errorMessage = exc.Message;
}
if (errorMessage == null)
{
filenameEntry.Text = "";
fileEditor.Text = "";
RefreshListView();
}
else
{
await DisplayAlert("TextFileTryout", errorMessage, "OK");
}
}
async void OnFileListViewItemSelected(object sender, SelectedItemChangedEventArgs args)
{
if (args.SelectedItem == null)
return;
string filename = (string)args.SelectedItem;
string errorMessage = null;
try
{
fileEditor.Text = fileHelper.ReadText((string)args.SelectedItem);
filenameEntry.Text = filename;
}
catch (Exception exc)
{
errorMessage = exc.Message;
}
if (errorMessage != null)
{
await DisplayAlert("TextFileTryout", errorMessage, "OK");
}
}
void OnDeleteMenuItemClicked(object sender, EventArgs args)
{
string filename = (string)((MenuItem)sender).BindingContext;
fileHelper.Delete(filename);
RefreshListView();
}
void RefreshListView()
{
fileListView.ItemsSource = fileHelper.GetFiles();
fileListView.SelectedItem = null;
}
}
代码隐藏文件在三种情况下使用await运算符调用DisplayAlert:如果指定的文件名已存在,则“保存”按钮将使用DisplayAlert。这证实您的真实意图是替换现有文件。另外两个用途是用于通知保存或加载文件时发生的错误。文件保存和文件加载操作位于try和catch块中
捕捉可能发生的任何错误。例如,文件保存操作将因非法文件名而失败。在读取文件时遇到错误的可能性较小,但程序仍会检查。
可以想象,在没有await运算符的情况下可以显示通知用户错误的警报,但是他们仍然使用await来演示异常处理中涉及的基本原则:尽管C#6允许在catch块中使用await,但C#5却没有。为了解决这个限制,catch块只是将错误消息保存在名为errorMessage的变量中,然后catch块后面的代码使用DisplayAlert显示该文本(如果存在)。此结构允许这些事件处理程序根据成功完成或错误以不同的处理结束。
另请注意,构造函数以对RefreshListView的调用结束,以显示ListView中的所有现有文件,并且代码隐藏文件在保存新文件或删除文件时也调用该方法。
但是,此程序在Windows平台上不起作用。我们来解决这个问题。