PCL Storage (2)

Keywords:   Xamarin.Forms, PCL Storage, ListView, Mac Finder, Windows Explorer, Popup Menu, TapGestureRecognizer, Double Click, ContextActions, CommandParameter, {Binding .}, ListView.Footer, RowHeight of Grid where Label is placed, Border Color of Entry, INotifyPropertyChanged, ObservableCollection, OnSizeAllocated, Recursively Sort Hierarchy Data, NewGuid



In the post PCL Storage (1), I studied Folder and File I/O in Local Storage, but the User Interface (UI) of the App was not absolutely interesting.  The App in the previous post, Command Prompt (1), is the one that I tried to make its UI more interesting (PCL Storage function has not been built into the Command Prompt App yet. I will make its development and post it as Command Prompt(2). ).  But I had an idea that Mac-Finder-Like-Viewer (Windows-Explorer-Like-View in other words) is also useful UI for folder and file I/O, so I introduce the development.

Before getting to the main subject, let me explain further about the title of this post.  It's PCL Storage (2), but I just modified the PCL Storage methods a little, and there was not anything I have to precisely explain regarding them.  More than that, I have learned many things through the Mac-Finder-Like-Viewer development, so the title of this post can be Mac-Finder-Like-Viewer (1).  And also, I could learned also ListView in the development, the title can also be ListView (3) that follows ListView(1) and (2).

Firstly, I have to explain how to manage objects (folders and files) in this App.  Since the PCL Storage is used in this App, 'actual' folders and files will be created.  The folders and files can be managed by checking their existence, but it must be complicated in some sort of processing such as the case of getting the parent folder of a folder or file.  So I defined files, folders, and these folder structure as Class and ObservableCollection and manage them in this App apart from the actual folders and files.

OK, let's dive into the source code.  Screen design and UI, folder and file management, PCL Storage methods are explained in MainPage.xaml, its code behind MainPage.xaml.cs, and PCLStrg.cs respectively below.


The screenshot just after launching this App (left) and a screenshot when a few folders and files are created (right).  You can see the tree structure of the created objects.

  


MainPage.xaml
<?xml version="1.0" encoding="UTF-8"?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms" 
        xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" 
        xmlns:local="clr-namespace:PCLStrg;assembly=PCLStrg" 
        x:Class="PCLStrg.MainPage">

    <StackLayout Margin="5, 30, 5, 50">

        <!-- App Title -->
        <StackLayout BackgroundColor="Gray" Padding="0,0,0,1">
            <Label Text="PCL Storage Demo with Mac-Finder-Like-Viewer" 
                   FontSize="16" BackgroundColor="White" />
        </StackLayout>

        <!-- Mac-Finder-Like-Folder/File-Viewer -->
        <StackLayout BackgroundColor="Gray" Padding="1" VerticalOptions="FillAndExpand">
            <StackLayout BackgroundColor="White" VerticalOptions="FillAndExpand" >
                
                <Label Text="Finder: Folders and Files Viewer" TextColor="White" BackgroundColor="Gray"/>                
                
                <!-- Place ListView in AbsoluteLayout to enable popup menu -->
                <AbsoluteLayout x:Name="BackGround" VerticalOptions="FillAndExpand" SizeChanged="SetFinderSize" >                    

                    <StackLayout x:Name="FinderBase" BackgroundColor="White" Spacing="0">
                        
                        <!-- Back Button, Display Current Folder, Create Object Button -->
                        <StackLayout HeightRequest="30" Orientation="Horizontal">
                            <Button x:Name="BackToParentButton" Text="&lt;" TextColor="White" FontSize="20" 
                                    Clicked="BackToParent" IsEnabled="false"/>
                            <Entry x:Name="CurrentFolderEntry" HeightRequest="12" HorizontalOptions="FillAndExpand"/>
                            <Button Text="+" FontSize="20" Clicked="PopUpCreateObjectMenu"/>
                        </StackLayout>                        
                        
                        <!-- Finder(Main Area): Objects (Folders and Files) Viewer -->
                        <ListView x:Name="Finder" ItemSelected="ObjectSelected" BackgroundColor="White"
                                SeparatorVisibility="None" RowHeight="20" >
                            <!-- Item: Folder or File -->
                            <ListView.ItemTemplate>
                                <DataTemplate>
                                    <ViewCell Height="20" >
                                        
                                        <StackLayout Orientation="Horizontal" Margin="3,0,0,0">
                                            
                                            <!-- Indent Item Position according to the Depth -->
                                            <StackLayout WidthRequest="{Binding Indent}"/>

                                            <!-- Icon showing Expanded status of each Folder-->
                                            <Button Image="{Binding Icon}" Clicked="FolderExpandCollapse" 
                                                    CommandParameter="{Binding .}"/>

                                            <!-- Name of Object (Folder or File) -->
                                            <Label Text="{Binding NameWithExtension}" FontSize="16" TextColor="{Binding Color}" >
                                                <Label.GestureRecognizers>
                                                    <TapGestureRecognizer Tapped="ObjectDoubleTapped" NumberOfTapsRequired="2"/>
                                                </Label.GestureRecognizers>                                                
                                            </Label>

                                            <!-- The Number of children of this Object -->
                                            <Label Text="{Binding NoChildrenText}" FontSize="16" TextColor="Gray" />
                                        </StackLayout>

                                        <!-- ContextActions Menu: Open/Edit, Delete, Others -->
                                        <ViewCell.ContextActions >
                                            <MenuItem Clicked="OtherObject" Text="Others"  CommandParameter="{Binding .}" />
                                            <MenuItem Clicked="OpenObject" Text="{Binding OpenAction}"  CommandParameter="{Binding .}" />
                                            <MenuItem Clicked="DelObject" Text="Delete" CommandParameter="{Binding .}" IsDestructive="true" />                                            
                                        </ViewCell.ContextActions>                                        
                                        
                                    </ViewCell>
                                </DataTemplate>
                            </ListView.ItemTemplate>

                            <!-- Footer : PopUp Menu of Paste etc. -->
                            <ListView.Footer>
                                <Button x:Name="Footer" Clicked="FooterClicked"/>
                            </ListView.Footer>
                            
                        </ListView>                        

                    </StackLayout>


                    <!-- PopUp Menu to create Folder or File -->
                    <Grid x:Name="PopUpMenu" BackgroundColor="Gray" IsVisible="false" 
                          Padding="1" ColumnSpacing="1" RowSpacing="5"
                          WidthRequest="200" TranslationX="150" TranslationY="100">
                        <Grid.RowDefinitions>
                            <RowDefinition Height="Auto"/>
                            <RowDefinition Height="Auto"/>
                            <RowDefinition Height="Auto"/>
                            <RowDefinition Height="Auto"/>
                        </Grid.RowDefinitions>
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition Width="0.5*"/>
                            <ColumnDefinition Width="0.5*"/>
                        </Grid.ColumnDefinitions>

                        <!-- Title of this PopUp Menu-->
                        <BoxView BackgroundColor="White" Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2" />
                        <Label Text="Create Object" HorizontalOptions="Center" VerticalOptions="Center"
                                Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2" />
                        
                        <!-- Input Object Name to be created -->
                        <Entry x:Name="ObjectName" Text="" Placeholder="Input Object Name" 
                                Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2"/>
                        
                        <!-- Change the default Color of Entry Boderline, Blue, to Gray -->
                        <Button x:Name="DeleteEntryBorder" BackgroundColor="Transparent" InputTransparent="True" 
                                BorderColor="Gray" BorderWidth="2" 
                                Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2"/>

                        <!-- Select Folder or File to be created -->
                        <Button x:Name="FolderButton" Text="Folder" Clicked="CreateFolder" 
                                Grid.Row="2" Grid.Column="0" BackgroundColor="White" />
                        <Button x:Name="FileButton" Text="File" Clicked="CreateFile" 
                                Grid.Row="2" Grid.Column="1" BackgroundColor="White" />
                        
                        <!-- OK: Create Object,  Cancel: Hide the Popup Menu -->
                        <Button Text="OK" Clicked="OKCreateObject" 
                                Grid.Row="3" Grid.Column="0" BackgroundColor="White" />
                        <Button Text="Cancel" Clicked="CancelCreateObject" 
                                Grid.Row="3" Grid.Column="1" BackgroundColor="White" />
                    </Grid>


                    <!-- PopUp File Editor -->
                    <Grid x:Name="PopUpEditor" BackgroundColor="Gray" IsVisible="false" 
                          Padding="1" RowSpacing="1" TranslationX="150" TranslationY="50">

                        <Grid.RowDefinitions>
                            <RowDefinition Height="Auto"/>
                            <RowDefinition Height="Auto"/>
                            <RowDefinition Height="Auto"/>
                        </Grid.RowDefinitions>
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition Width="Auto"/>                            
                        </Grid.ColumnDefinitions>

                        <!-- Title of this PopUp Menu-->
                        <Label x:Name="FileEditorTitle" BackgroundColor="White"
                                Grid.Row="0" Grid.Column="0"/>

                        <!-- File Editor -->
                        <Editor x:Name="FileEditor" BackgroundColor="White" 
                                WidthRequest="200" HeightRequest="300" 
                                Grid.Row="1" Grid.Column="0"/>

                        <!-- Button for Save File and Cancel -->
                        <StackLayout BackgroundColor="White" Grid.Row="2" Grid.Column="0">
                            <StackLayout Orientation="Horizontal" HorizontalOptions="EndAndExpand">
                                <Button Text="Save" TextColor="Black" Clicked="SaveFile" HeightRequest="20"
                                        BackgroundColor="White" BorderWidth="1" />
                                <Button Text="Cancel" TextColor="Black" Clicked="CancelEditor" HeightRequest="20"
                                        BackgroundColor="White" BorderWidth="1" />
                            </StackLayout>
                        </StackLayout>
                    </Grid>

                </AbsoluteLayout>

            </StackLayout>
        </StackLayout>
    </StackLayout>

</ContentPage>
(The source code is also here)

In order to draw a gray borderline with the thickness=1, the StackLayout with white background color are placed in the another StackLayout with gray background color and padding=1.  The main area of this App screen is placed in the AbsoluteLayout "BackGround" to enable pop up menu explained later.  At the top of "BackGround," Button, Entry, and one more Button are horizontally placed.  The left Button "BackToParentButton" with "<" sign is to return back to the parent folder.  The Entry "CurrentFolderEntry" displays the current path.  If the right Button with "+" sign is pressed, a menu, explained later, is popped up to create a new folder or file.

Below these Buttons and Entry is the ListView "Finder" displaying folders and files.  Each item (folder or file) of "Finder" are composed of a couple of parts horizontally placed in ViewCell.  The StackLayout with WidthRequest binding "Indent" is to indent (slide the position to right) each item according to its depth in the folder structure.  Next to this StackLayout is the Button, whose Image property binds three Icon images corresponding to expanded folder, collapsed folder and file.  The expanded and collapsed folder icon are toggle switched, if they are pressed.  On the right side of this Button, the Label displaying folder and file name is placed.  TapGestureRecognizer enable to detect double-click to this Label.  Binding "Color" to the TextColor property of this Label, folder and file name color becomes blue and black respectively.  At the right end of the item, if the item is a folder, the number of subfolders and files that the folder has, "NoChildrenText", is placed.  Each item has ContextActions, and its MenuItem are "Others," "Open/Edit," "Delete."  Regarding "Open/Edit," "Open" for folder and "Edit" for file is shown in MenuItem through binding "OpenAction".  CommandParameter="{Binding .}" in ContextActions enables to get the selected item.  I will explain more deeply in code-behind.

Footer is added at the bottom of the ListView "Finder".  It looks like a empty space, and enable to pop up a menu including 'create a new folder', 'paste', and whatever by tapping it.  It's like right-click on empty space in Mac Finder.

Next is the two Grids.  The first one, "PopUpMenu," is popped up by "+" sign Button for new folder/file creation.  The menu is composed of the Label for the menu title, the Entry "ObjectName" for new folder/file name input, the two Buttons to select folder or file to be created, and the two Buttons to do the creation or cancel it.  Here, I had a problem regarding row height of Grid.  It was that the Grid cell where the menu title Label was placed couldn't have enough its row height.  So I firstly placed a BoxView in the Grid cell, and placed the Label after that, then the problem was solved (But I still don't know why).  And one more thing in this popup menu.  It's about the Button "DeleteEntryButton" that is placed the same Grid cell as the Entry "ObjectName."  Overwriting the Button on the Entry can change the color on the Entry to the one (Gray in this case) of the Button (Nothing other than such a dull way?).  The second Grid "PopUpEditor" is the file editor.  An Editor view below the "FileEditorTitle" Label displaying the file name being edited, and the "Save" and "Cancel" Buttons at the bottom of this menu.  Finally, the IsVisible prperty of both of two Grids are 'false.'  So, when this App is launched, these two menus are not popped up, and making the property true enable to pop up them from the code-behind.


Moving on to the C# code-behind.

MainPage.xaml.cs
using System;
using System.Linq;
using System.Collections.ObjectModel;
using System.ComponentModel;
using Xamarin.Forms;
using PCLStorage;

namespace PCLStrg
{
    public partial class MainPage : ContentPage
    {
        // Define Object (Folder/File) class
        class Object : INotifyPropertyChanged
        {            
            public string Name { getset; }    // Name of Object
            public string NameWithExtension     // If File, add "txt" extention to the name
            {
                get { return IsFolder ? Name : Name+".txt"; }
            }

            public bool IsFolder { getset; }  // Folder -> true, File -> false

            // Notify the change of IsExpanded
            private bool isexpanded;
            public event PropertyChangedEventHandler PropertyChanged;
            public bool IsExpanded
            {
                get { return this.isexpanded; }
                set
                {
                    if (this.isexpanded != value)
                    {
                        this.isexpanded = value;
                        OnPropertyChanged("IsExpanded");
                        OnPropertyChanged("Icon");
                    }
                }
            }
            protected virtual void OnPropertyChanged(string propertyName)
            {
                if (PropertyChanged != null)
                {
                    PropertyChanged(thisnew PropertyChangedEventArgs(propertyName));
                }
            }
            
            public Object Parent { getset; }  // Parent Folder of this Object
            
            public int NoChildren { getset; } // Number of Children in this Object            
            public string NoChildrenText        // Text of NoChildren
            {
                get { return IsFolder ? "(" + NoChildren.ToString() + ")" : ""; }
            }
            
            public string Path { getset; }    // Directory Path
            
            public string Color                 // Text Color: Folder->Blue, File->Black
            {
                get { return IsFolder ? "Blue" : "Black"; }
            }
            
            public string Icon          // Icon: Expanded Folder -> expanded.png
                                        //       Collapsed Folder -> collapsed.png
                                        //       File -> file-hyphen.png
            {
                get { return IsFolder ? (IsExpanded ? "expanded.png" : "collapsed.png") : "file-hyphen.png"; }
            }
            
            public int Depth { getset; }  // Depth in Folder Structure            
            public int Indent               // Indent Display Position according to the Depth
            {
                get { return Depth * 15; }
            }
            
            public string OpenAction    // ContextAction Menu: Folder->Open, File->Edit
            {
                get { return IsFolder ? "Open" : "Edit"; }
            }

            // PCL Storage: IFolder/IFile of this Object
            public IFolder iFolder { getset; }
            public IFile iFile {getset;}

        } // End of "class Object"

        ObservableCollection<ObjectCurrentDisplayObjects
            = new ObservableCollection<Object>();
        ObservableCollection<ObjectAllObjects
            = new ObservableCollection<Object>();

        Object CurrentFolder = new Object();

        static Object RootFolder = new Object
        {
            Name = "Root",
            IsFolder = true,
            IsExpanded = false,
            Parent = null,
            Path = "Root>",
            Depth = 0
        };
        

        public MainPage()
        {
            InitializeComponent();            

            CreateRootFolder();
            UpdateFinder(CurrentDisplayObjects);            
        }


        // Resize the ListView "Finder" by expanding the StackLayout "FinderBase" size
        // Two methods, 1 and 2, below are effective to do it, but 
        //    iOS: Both of 1 and 2
        //    UWP: Only 2             are effective respectively.
        /*
        // 1
        protected override void OnSizeAllocated(double width, double height)
        {
            base.OnSizeAllocated(width, height);

            // Set "Footer" space at the bottom of ListView.
            // Tap the "Footer" space, Pop up menu of some commands
            Footer.HeightRequest = BackGround.Height - 100;

            // Fit the size of ListView "Finder" to the space            
            FinderBase.HeightRequest = BackGround.Height;
            FinderBase.WidthRequest = BackGround.Width;
        }
        */
        // 2
        void SetFinderSize(object senderEventArgs e)
        {
            // Set "Footer" space at the bottom of ListView.
            // Tap the "Footer" space, Pop up menu of some commands
            Footer.HeightRequest = BackGround.Height - 100;

            // Fit the size of ListView "Finder" to the space
            FinderBase.HeightRequest = BackGround.Height;
            FinderBase.WidthRequest = BackGround.Width;
        }


        // Create the RootFolder in the root directory of LocalStorage
        void CreateRootFolder()
        {
            CurrentFolder = RootFolder;

            // PCL Storage: Create the IFolder of RootFolder (RootFolder.iFolder)
            ICreateFolder(RootFolder, IRootFolder);
        }


        // Display all updated Objects on the ListView "Finder"
        void UpdateFinder(ObservableCollection<Objectobjects)
        {
            // Bind the sorted Objects to ListView "Finder"
            CurrentDisplayObjects = SortObject(objects);
            Finder.ItemsSource = CurrentDisplayObjects;

            // Display the Current Path
            CurrentFolderEntry.Text = CurrentFolder.Path;

            // In RootFolder, disable BackToParent Button
            if (CurrentFolder == RootFolder)
            {
                BackToParentButton.TextColor = Color.Yellow;
                BackToParentButton.IsEnabled = false;
            }
            else
            {
                BackToParentButton.TextColor = Color.Black;
                BackToParentButton.IsEnabled = true;
            }              
        }

        
        // Sort all Objects (Folders&Files) in a Folder
        ObservableCollection<ObjectSortObject(ObservableCollection<Objectobjects)
        {
            ObservableCollection<ObjectFolders = new ObservableCollection<Object>();
            ObservableCollection<ObjectFiles = new ObservableCollection<Object>();
            ObservableCollection<ObjectsubObjects = new ObservableCollection<Object>();
            ObservableCollection<ObjectsortedObjects = new ObservableCollection<Object>();

            // Get minimum Depth of the Objects
            int minDepth = int.MaxValue;
            foreach(Object o in objects)
            {
                if (minDepth > o.DepthminDepth = o.Depth;
            }
            
            // Separate Folders&Files with the Depth of minDepth
            foreach(Object o in objects)
            {
                if(o.Depth==minDepth)
                {
                    if (o.IsFolderFolders.Add(o);
                    else            Files.Add(o);
                }
            }

            // Sort Folders&Files by Name respectively
            sortedObjects = new ObservableCollection<Object>(Folders.OrderBy(n => n.Name));
            Files = new ObservableCollection<Object>(Files.OrderBy(n => n.Name));            

            // Recursively sort all subObject located within all Expanded Folders
            foreach(Object o in Folders)            
            {
                if(o.IsExpanded)
                {
                    // Collect all objects whose parent is "o"
                    foreach(Object oChild in AllObjects)
                    {
                        if (oChild.Parent == osubObjects.Add(oChild);
                    }

                    // Recursive call: 
                    //      Sort the subObjects located within this object "o"
                    subObjects = SortObject(subObjects);
                    
                    // Insert the sorted subObjects below its parent object "o"
                    int i = sortedObjects.IndexOf(o)+1;
                    foreach(Object sub_o in subObjects)
                    {
                        sortedObjects.Insert(isub_o);
                        i++;
                    }
                }
            }

            // Finally, add Files to the end of sortedObjects
            foreach(Object o in Files)
            {
                sortedObjects.Add(o);
            }

            return sortedObjects;
        }


        // Expand/Collapse Folder
        void FolderExpandCollapse(object senderEventArgs e)
        {
            // Get index of Folder whose IsExpanded was switched
            int index = CurrentDisplayObjects.IndexOf(
                ( (Object)( (Button)sender ).CommandParameter )    );

            // Toggle switch IsExpanded
            CurrentDisplayObjects[index].IsExpanded = !CurrentDisplayObjects[index].IsExpanded;

            UpdateFinder(CurrentDisplayObjects);            
        }


        // Pop up Menu to create a new Object
        void PopUpCreateObjectMenu(object senderEventArgs args)
        {
            // default: create Folder
            isFolder = true;
            FolderButton.TextColor = Color.Black;
            FileButton.TextColor = Color.LightGray;

            PopUpMenu.IsVisible = true;     // Pop up Create-Folder-Menu

            // Set Entry "ObjectName" Keyboard
            //      Plain: No Characterize and Spell Check
            ObjectName.Keyboard = Keyboard.Plain;
            
            ObjectName.Focus();     // Set cursor on Entry "ObjectName"
        }


        // Select Folder/File to be created
        bool isFolder = new bool();        
        void CreateFolder(object senderEventArgs e)   // Folder Create Mode
        {
            isFolder = true;
            FolderButton.TextColor = Color.Black;
            FileButton.TextColor = Color.LightGray;
        }        
        void CreateFile(object senderEventArgs e)     // File Create Mode
        {
            isFolder = false;
            FolderButton.TextColor = Color.LightGray;
            FileButton.TextColor = Color.Black;
        }

        // OK Button pressed, and create a new Object
        void OKCreateObject(object senderEventArgs e)
        {
            Object newObject = new Object
            {
                Name = ObjectName.Text,
                IsFolder = isFolder
                IsExpanded = false,
                Parent = CurrentFolder,
                Depth = CurrentFolder.Depth + 1
            };
            
            // Create a new Object with the random name, if no Object's name input
            if (newObject.Name == "")                
                newObject.Name = Guid.NewGuid().ToString("N").Substring(03);

            // Check the existence of newObject
            foreach(Object o in CurrentDisplayObjects)
            {
                if(o.Parent == CurrentFolder// Has the same Parent?
                {
                    if(o.IsFolder==isFolder)  // Folder or File?
                    {
                        if (o.Name == ObjectName.Text){
                            DisplayAlert(""o.NameWithExtension + " already exists!""OK");
                            return;
                        }
                    }
                }
            }


            // Update Path name
            newObject.Path = newObject.Parent.Path + newObject.Name + ">";
            if (newObject.Path.Length > 50// If Path name is too long, shorten it.
            {
                string p = newObject.Path.Substring(newObject.Path.Length - 45);
                newObject.Path = "..." + p;
            }
            
            newObject.Parent.NoChildren++;  // Increment the number of children

            // Add newObject to the both of the displayed Objecs and all Objects
            CurrentDisplayObjects.Add(newObject);
            AllObjects.Add(newObject);

            UpdateFinder(CurrentDisplayObjects);

            // Clear the text in ObjectName Entry and focus it again
            ObjectName.Text = "";
            ObjectName.Focus();


            // PCL Storage: Create a new IFolder/IFile corresponding to the newObject
            if (isFolder) ICreateFolder(newObjectCurrentFolder.iFolder);
            else ICreateFile(newObjectCurrentFolder.iFolder);

        }

        // Press Cancel Button, hide the PopUp Menu
        void CancelCreateObject(object senderEventArgs e)
        {
            PopUpMenu.IsVisible = false;
        }

        
        // Folder/File is selected
        void ObjectSelected(object sender, SelectedItemChangedEventArgs e)
        {
            Object selectedObject = (Object)e.SelectedItem;

            /* Switch Displaying Buttons for processing folders/files ? */            
        }       


        // Folder/File is double clicked
        async void ObjectDoubleTapped(object senderEventArgs e)
        {
            Label tappedLabel = (Label)sender;
            

            // Identify the tapped object
            foreach ( Object o in CurrentDisplayObjects )
            {
                if(o.NameWithExtension == tappedLabel.Text)
                {
                    if(o.IsFolder)  // If Folder, move to there
                    {
                        CurrentFolder = o;
                        break;
                    }
                    else            // If File, open it
                    {
                        openedFile = o;
                        FileEditorTitle.Text = "File Name: " + o.NameWithExtension;
                        FileEditor.Keyboard = Keyboard.Plain;
                        PopUpEditor.IsVisible = true;
                        FileEditor.Text = await IReadFile(o.iFile);
                        //IOpenFile(o, (Editor)FileEditor);
                        return;
                    }
                }                
            }
            CurrentDisplayObjects = GetCurrentDisplayObjects(CurrentFolder);
            UpdateFinder(CurrentDisplayObjects);
        }


        // Get all display Objects in Folder 
        ObservableCollection<ObjectGetCurrentDisplayObjects(Object folder)
        {
            ObservableCollection<ObjectNewDisplayObjects 
                            = new ObservableCollection<Object>();

            // Get all objects whose parent is tappedObject
            foreach (Object o in AllObjects)
            {
                if (o.Parent == folder)
                {                    
                        NewDisplayObjects.Add(o);
                }
            }            
            return NewDisplayObjects;
        }


        // Back to the Parent Folder
        void BackToParent(object senderEventArgs e)
        {
            // If in RootFolder, don' do anything
            if (CurrentFolder == RootFolderreturn;

            // Change the CurrentFolder
            CurrentFolder = CurrentFolder.Parent;
            CurrentDisplayObjects = GetCurrentDisplayObjects(CurrentFolder);
            UpdateFinder(CurrentDisplayObjects);
        }

        
        // ContextActions: Open/Edit, Delete, Others
        Object openedFile = new Object();
        async void OpenObject(object senderEventArgs e)
        {
            Object o = (Object)((MenuItem)sender).CommandParameter;
            openedFile = o;
            
            if (o.IsFolder// If Folder, move to there
            {
                CurrentFolder = o;
                CurrentDisplayObjects = GetCurrentDisplayObjects(CurrentFolder);
                UpdateFinder(CurrentDisplayObjects);
            }
            else            // If File, open it
            {
                FileEditorTitle.Text = "File Name: " + o.NameWithExtension
                FileEditor.Keyboard = Keyboard.Plain;
                PopUpEditor.IsVisible = true;
                FileEditor.Text = await IReadFile(o.iFile);
            }
        }

        void DelObject(object senderEventArgs e)
        {
            Object o = (Object)((MenuItem)sender).CommandParameter;
            CurrentDisplayObjects.Remove(o);
            UpdateFinder(CurrentDisplayObjects);

            // PCL Storage: Delete IFolder/IFile of "o"
            if (isFolder) IDeleteFolder(o.iFoldero.Parent.iFolder);
            else IDeleteFile(o.iFileo.Parent.iFolder);
        }

        // Pop Up Others Processing Menu including Copy, Cut, Paste, Rename
        void OtherObject(object senderEventArgs e)
        {

        }

        // Buttons of PopUpEditor Menu: save File, cancel Editor
        void SaveFile(object senderEventArgs e)
        {
            // PCL Storage: Write content in IFile
            IWriteFile(openedFile.iFile, FileEditor.Text);
        }
        void CancelEditor(object senderEventArgs e)
        {
            PopUpEditor.IsVisible = false;
        }

        // 
        void FooterClicked(object senderEventArgs e)
        {
            
        }


    } // End of ™public partial class MainPage : ContentPage™
// End of "namespace PCLStrg"

The properties of folder and file are defined in a little bit large "Object" class that inherits from INotifyPropertyChanged class.  Each properties are below:

  • Name:  The name of this object (folder or file)
  • NameWithExtension:  Add the extension ".txt" to the Name, if file. 
  • IsFolder:  True, if folder.  False, if file.  This is used to switch several other properties.
  • IsExpanded:  Expand (true) /Collapse (false) status of the folder.  Flase, if file.  The expanded folder shows the subfolders and the files in it.
  • Parent:  The parent folder of this object
  • NoChildren:  The number of children of the folder.  0 (Zero), if file.
  • NoChildrenText:  Text of NoChildren.  "", if file.
  • Path:  The path name under LocalStorage
  • Color:  Blue, if folder.  Black, if file.
  • Icon:  Downward and right triangle for expanded and collapsed folder respectively,  hyphen for file
  • Depth:  The depth in the folder hierarchy under LocalStorage
  • Indent:  The indent width of the object display position according to the Depth
  • OpenAction:  The MenuItem name in ContextActions.  "Open," if folder.  "Edit," if file.
  • iFolder:  IFolder object.  null, if file.
  • iFile:  IFile object.  null, if folder.


The "CurrentDisplayObjects" is the objects collection currently displayed in "Finder."  The "AllObjects" is all of the objects including not-displayed objects located in collapsed folders in "Finder."  And also define the "CurrentFolder" and the "RootFolder."  In "MainPage," create the "RootFolder" firstly, and update "Finder" sequentially by the "UndateFinder" method after that.

The "OnSizeAllocated" and "SetFinderSize" method just after the "MainPage" can correct the size of the StackLayout "FinderBase" wrapping the ListView "Finder."  (The ListView.Footer's height is also calculated here, but it is not important for this size correction problem.)  If this size correction is not executed, the "FinderBase" remains small, the "Finder" couldn't be displayed properly in terms of its height.  And, as I wrote in the source code, both of the "OnSizeAllocated" and "SetFinderSize" are OK for iOS, but the "OnSizeAllocaated" throw an error in UWP (I don't know why.)  BTW, in the previous post Command Prompt (1), there was a problem regarding ScrollView size.  It could be solved by this way.  Anyway, I suppose that the size of Layout and View is usually unstable just after launching Apps, so I have to learn the issue more.

The "UpdateFinder" method updates the objects displayed in "Finder."  It sorts its argument "objects" and send it "Finder," displays the current path, disable '<' Button if the current folder is "RootFolder."

The "SortObject" method sorts its arugument "objects" and returns it as below.  Firstly, it gets the minimum depth "minDepth" of the "objects" (in most cases, "minDepth" is the "CurrentFolder.Depth"), separates folders and files with the Depth of "minDepth," and separately sort them by Name.  Then, analyze the sorted folders "Folders."  The collapsed folders don't need any more processing.  For the each expanded (IsExpanded property = true) folder, find the all children "oChild" whose parent is the expanded folder from the "AllObjects."  And call "SortObject" recursively against the "subObjects (the collection of "oChild")" to deeply sort all objects of "objects."  Insert the sorted "subObjects" just below its parent "o," add the sorted files "Files" to its end, and finally return the sorted objects.

The "FolderExpandCollapse" method is raised by the click of the folder triangle icon.  Get the index of the clicked folder and invert (toggle) the IsExpanded property of the folder.  The change of the IsExpanded and Icon property is notified to the ListView "Finder" immediately, and the expand/collapse status of the folder is switched.  I learned how to use CommandParameter and {Binding .} to get the ListView item.

The "PopUpCreateObjectMenu" method pop up "PopUpMenu" by making its IsVisible property true.  The default object to be created is folder.  Set the Keyboard property of the Entry "ObjectName" Plain to disable characterization and spell check, and set cursor (focus) on the Entry.

The "CreateFolder" and "CreateFile" methods switch the color of the "FolderButton" and "FileButton" to show which object will be created.

The "OKCreateObject" method is raised by the click of the OK Button in the "PopUpMenu," and creates the "newObject."  If no input in the Entry "ObjectName," create three digit random text for the "newObject.Name."  After checking the existence of the object with the same name as the "newObject," update the current path name.  If the path name is too long to display in "CurrentFolderEntry," it is shorten properly.  And increment "NoChildren," add this "newObject" to both of the "CurrentDisplayObjects" and the "AllObjects," update the "Finder," clear the Entry "ObjectName" and set cursor in it.  Finally, create the actual folder or file by PCL Storage.

The "CancelCreateObject" method change the IsVisible property of "PopUpMenu" false and hides the menu.

The "ObjectSelected" method is raised by the ListView "Finder" item selection, but no function this time.

The "ObjectDoubleTapped" method is raised by the double click to objects.  Identify the double clicked object.  If the object is folder, change the current folder to the folder.  If the object is file, pop up "PopUpEditor."

The "GetCurrentDisplayObjects" method gets and returns the all objects located in the argument "folder."

The "BackToParent" method changes the "CurrentFolder" to the "CurrentFolder.Paret" and update the "Finder."  But if the current folder is the "RootFolder," it don't do anything and return immediately.

The "OpenObject," "DelObject," and "OtherObject" methods are linked with the ContextActions MenuItem.  In "OpenObject," move to the folder if the selected object is folder, pop up "PopUpEditor" if the selected object is file.  The "DelObject" deletes the selected object.  The "OtherObject" do nothing this time.

The "SaveFile" and "CancelEditor" method save or don't save the content of the "FileEditor."

The "FooterClicked" method is raised by the click to the Footer of the ListView "Finder," but there is no processing this time.  In the next version, it will pop up a menu including paste command etc.



Let's move on to the final code, PCLStrg.cs.  I modified the arguments of the methods and made them more general than the ones in PCL Storage (1).  I think that a detailed explanation is not necessary.  At the end of this code, several empty methods with the name including the words, copy/cut/paste object.  I will developed them in the next version.

PCLStrg.cs
using System.Collections.Generic;
using System.Threading.Tasks;

using Xamarin.Forms;

using PCLStorage;

namespace PCLStrg
{
    public partial class MainPage : ContentPage
    {
        //Get Root Folder Path
        IFolder IRootFolder = FileSystem.Current.LocalStorage;
        // Root Name: IRootFolder.Name
        // Root Path: IRootFolder.Path


        //Create a new IFolder "folder.iFolder" 
        //  corresponding to the Object "folder" in the IFolder "parent".
        //  (If the IFolder already exists, move there.)
        async void ICreateFolder(Object folder, IFolder iparent)
        {
            folder.iFolder =
                      await iparent.CreateFolderAsync(folder.Name, CreationCollisionOption.ReplaceExisting);
        }


        // Delete IFolder "ifolder" in IFolder "iparent"
        async void IDeleteFolder(IFolder ifolder, IFolder iparent)
        {
            // Check the existence of the IFolder corresponding to the Object "folder"
            bool IsExist = await ICheckFolderExist(ifolderiparent);
            if( ! IsExist)
            {
                await DisplayAlert(""ifolder.Name.ToString() + " doesn't exist""OK");
                return;
            }            

            await ifolder.DeleteAsync();
        }


        // Check existence of IFolder "ifolder" in IFolder "iparent"
        async Task<boolICheckFolderExist(IFolder ifolder, IFolder iparent)
        {
            ExistenceCheckResult IsExist = await iparent.CheckExistsAsync(ifolder.Name);
            if (IsExist == ExistenceCheckResult.FolderExists) return true;
            return false;
        }


        // Get All IFolders in the IFolder "iparent"
        async void IGetFolders(IFolder iparent)
        {
            IList<IFolder> folders = await iparent.GetFoldersAsync();
        }


        // Delete all of IFolders and IFiles in IFolder "iparent"
        async void IDleteAllObjects(IFolder iparent)
        {
            // Get ALl IFolders and IFiles in the IFolder "parent"
            IList<IFolder> ifolders = await iparent.GetFoldersAsync();
            IList<IFile> ifiles = await iparent.GetFilesAsync();

            // Delete All IFolders
            foreach(IFolder ifolder in ifolders)
            {
                await ifolder.DeleteAsync();
            }

            // Delete All IFiles
            foreach (IFile ifile in ifiles)
            {
                await ifile.DeleteAsync();
            }

            // Clear All Objects (Folders&Files)
            CurrentDisplayObjects.Clear();
            AllObjects.Clear();
            UpdateFinder(CurrentDisplayObjects);
        }



        //Create a new IFile "file.iFile" 
        //   corresponding to the Object "file" in the IFolder "iparent".
        //  (If the IFile already exists, open it.)
        async void ICreateFile(Object file, IFolder iparent)
        {
            file.iFile =
                    await iparent.CreateFileAsync(file.NameWithExtension, CreationCollisionOption.ReplaceExisting);
        }

        // Delete IFile "ifile" in IFolder "iparent"
        async void IDeleteFile(IFile ifile, IFolder iparent)
        {
            // Check the existence of "file"
            bool IsExist = await ICheckFileExist(ifileiparent);
            if (!IsExist)
            {
                await DisplayAlert(""ifile.Name.ToString() + " doesn't exist""OK");
                return;
            }

            await ifile.DeleteAsync();     // Delete the IFile "file"
        }

        // Check existence of IFile "ifile" in IFolder "iparent"
        async Task<boolICheckFileExist(IFile ifile, IFolder iparent)
        {
            ExistenceCheckResult IsExist = await iparent.CheckExistsAsync(ifile.Name);
            if (IsExist == ExistenceCheckResult.FileExists) return true;
            return false;
        }

        // Get All IFiles in in the IFolder "iparent"
        async void IGetFiles(IFolder iparent)
        {
            IList<IFile> ifiles = await iparent.GetFilesAsync();
        }

        // Write "content" to IFile "ifile"
        async void IWriteFile(IFile ifilestring content)
        {
            await ifile.WriteAllTextAsync(content);
        }

        // Read all text of IFile "ifile"
        async Task<stringIReadFile(IFile ifile)
        {
            string content = await ifile.ReadAllTextAsync();
            return content;
        }


        // Get All IFolders and IFiles in IFolder "parent"
        async void IGetAllObjects(IFolder parent)
        {
            await DisplayAlert("""Get All IFolders and IFiles in parent""OK");
        }

        // Copy, Cut, Paste IFolder
        async void ICopyFolder()
        {
            await DisplayAlert("""Copy IFolder""OK");
        }
        async void ICutFolder()
        {
            await DisplayAlert("""Cut IFolder""OK");
        }
        async void IPasteFolder()
        {
            await DisplayAlert("""Paste IFolder""OK");
        }

        // Copy, Cut, Paste IFile
        async void ICopyFile()
        {
            await DisplayAlert("""Copy IFile""OK");
        }
        async void ICutFile()
        {
            await DisplayAlert("""Cut IFile""OK");
        }
        async void IPasteFile()
        {
            await DisplayAlert("""Paste IFile""OK");
        }

    }
}


A movie showing this App running.  You can get the original movie file from here (in "iOS Simulator movie" folder).







コメント

このブログの人気の投稿

Get the Color Code from an Image Pixel

PCL Storage (1)

Optimal Rectangle Packing