跨平台檔案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平台上不起作用。我們來解決這個問題。