2017/7/27

Xamarin.Forms 教學系列文(二十.貳)File I/O



學習目標
  • PCL 實作 FileHelper
  • 檔案的寫入,讀取,刪除

自古以來,檔案存取 是程式最基本的功能,

不過手機取得檔案的方式和桌機有些不一樣,
在桌機上,使用者有權限去存取整個硬碟或瀏覽整個目錄,
在手機上,有幾個基本的資料夾專門拿來放圖片或音樂,甚至應用程式要存取的資料會放在 特定的資料夾 內。

有寫過 .NET 的程式人員一定會知道 System.IO 類別庫,支援檔案的輸入輸出,
其中最好用的就是 File 這個類別,
例如,
要寫入文字時可以直接使用 File.WriteAllText 方法,
或是讀取檔案時可以使用 File.ReadAllText 方法。

為了防止檔案存取去影響到 UI Thread,建議撰寫時都加上 非同步,教學見上一小節

好消息與壞消息

好消息是,在 Android 和 iOS 專案內,只要 using System.IO,都能使用完整的 File 類別與功能,
壞消息是,PCL 專案內並沒有 File 類別的實作 (聽說因為萬惡的Windows Phone 和 Android、iOS 存取方式不同),所以,乾脆就你自己來實作...

更好的消息是本篇不會介紹 Windows Phone 的實作(誤

實作跨平台 file I/O

總之有點無奈的,先在 PCL 建立介面
namespace TextFileTryout
{
    public interface IFileHelper
    {
        bool Exists(string filename);
        void WriteText(string filename, string text);
        string ReadText(string filename);
        IEnumerable GetFiles();
        void Delete(string filename);
    }
}

然後,在 iOS 專案內實作:
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 GetFiles()
        {
            return Directory.GetFiles(GetDocsPath());
        }

        public void Delete(string filename)
        {
            File.Delete(GetFilePath(filename));
        }

        // 取得資料夾檔案的路徑
        string GetFilePath(string filename)
        {
            return Path.Combine(GetDocsPath(), filename);
        }

        string GetDocsPath()
        {
            return Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
        }
    }
}

很蠢的是,Android 實作一模一樣,Copy & Past 到 Android 專案內,記得改 Dependency 和 namespace
至於 WP 我就不想介紹了…

最後最後,
為了讓使用上更方便,
我們回到 PCL 實作 IFileHelper 介面,並多一個取得所有檔案的方法:
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 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);
        }
    }
}

OK,準備好以上有點蠢的東西後 (感覺就是可以在 PCL 內建好給我們用的東西啊),我們來寫一個比較實際的東西


實際應用 File I/O

畫面上會有一個 Entry 和 ListView,
Entry 輸入值後可以存文字檔到手機內,並用 ListView 將資料夾內所有檔案叫出來,
當然我們也可以從 ListView 將這檔案刪除

XAML
*OnPlatform 寫法已更改,請參考此篇文章

<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-->
        <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>

.cs
為了讓事情 單純點,這邊沒有寫 MVVM,主要寫法為非同步事件
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");
        }
    }

    //點選 ListView 時
    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;
    }
}

執行結果:





沒有留言:

張貼留言