第二十章:非同步和檔案I/O.(九)
跨平臺檔案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平臺上不起作用。我們來解決這個問題。