Command Prompt (1)

I have been fighting with programming to develop an App I want to use, but this Command Prompt App will not be directory useful for the App. This is just a Xamarin.Forms programming training for me. I wanted to more learn ScrollView which behavior is a little bit confusing, and also to find a smart style to monitor some variables and App's behaviors while running App, so I came up with an idea to develop Command-Prompt-like-UI App looking like Unix or MS-DOS console. Including PCL Storage will make possible file and directory I/O, but it will be in other post. In this post, just a development of a Command-Prompt-Like-UI is explained.

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:CommandPrompt"
             x:Class="CommandPrompt.MainPage">

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

        <!-- App Title -->
        <StackLayout BackgroundColor="Gray" Padding="0,0,0,1">
            <Label Text="Command Prompt Demo:" BackgroundColor="White" />
        </StackLayout>

        <!-- Buttons for Debug -->
        <StackLayout BackgroundColor="Gray" Padding="0,0,0,1" >
            <StackLayout Orientation="Horizontal" BackgroundColor="White" Padding="0,0,0,2">
                <Label HorizontalTextAlignment="Center" VerticalTextAlignment="Start" 
                        FontSize="12" TextColor="Blue" >
                    <Label.FormattedText>
                        <FormattedString>
                            <Span Text="Debug"/>
                            <Span Text="{x:Static x:Environment.NewLine}" />
                            <Span Text="Buttons"/>
                        </FormattedString>
                    </Label.FormattedText>
                </Label>
                <Button Text="Button1" Clicked="Button1" 
                    WidthRequest="80" HeightRequest="30" Margin="3,3,0,3"
                    BorderColor="Blue" BorderWidth="1" />
                <Button Text="Button2" Clicked="Button2" 
                    WidthRequest="80" HeightRequest="30" Margin="3,3,0,3" 
                    BorderColor="Blue" BorderWidth="1" />
                <Button Text="Button3" Clicked="Button3" 
                    WidthRequest="80" HeightRequest="30" Margin="3,3,0,3" 
                    BorderColor="Blue" BorderWidth="1" />                
            </StackLayout>
        </StackLayout>        

        <!-- Command Prompt Console Area -->
        <StackLayout BackgroundColor="Gray" Padding="1" VerticalOptions="FillAndExpand">
            <StackLayout BackgroundColor="White" VerticalOptions="FillAndExpand" >
                <Label Text="Command Prompt Console" TextColor="White" FontSize="13" 
                       BackgroundColor="Gray" />
                <ScrollView x:Name="scrollView" >
                    <StackLayout x:Name="Console" Spacing="0" />
                </ScrollView>
            </StackLayout>
        </StackLayout>

        <!-- Data Display Area for Debug -->
        <StackLayout BackgroundColor="Gray" Padding="1" Orientation="Horizontal" 
                     HeightRequest="100" VerticalOptions="End" Spacing="1" >
            <StackLayout BackgroundColor="White" >
                <StackLayout.WidthRequest>
                    <OnPlatform x:TypeArguments="x:Double" iOS="120" WinPhone="180" />
                </StackLayout.WidthRequest>
                <Label Text="Debug ScrollView" TextColor="Blue" FontSize="12" />
                <ScrollView x:Name="debugScrollView" >
                    <StackLayout x:Name="debugResultList" Spacing="0" />
                </ScrollView>
            </StackLayout>
            <StackLayout BackgroundColor="White" WidthRequest="10" HorizontalOptions="FillAndExpand" >
                <Label Text="Debug Labels" TextColor="Blue" FontSize="12" />
                <Label x:Name="Label1" Text=" " FontSize="15" />
                <Label x:Name="Label2" Text=" " FontSize="15" />
                <Label x:Name="Label3" Text=" " FontSize="15" />
            </StackLayout>
        </StackLayout>

    </StackLayout>

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

Screen shot just after this App is launched.

Three Buttons are placed above the Main Console displaying command prompts. Those are to check App behaviors, and were actually useful for me in this development. Debug area and parts like these Buttons should be removed after development is completed, but I left them this time. The title of the Buttons, "Debug Buttons", are shown in two lines. It seems that FormattedText and Span can be used to insert Environment.Newline to Label text like this.

The Main Console is the StackLayout "Console" placed in the ScrollView "scrollView". By vertically adding Labels and Entries to "Console", Command-Prompt-Like behavior is realized, and it will be explained more precisely later. Actually, there are other implementations for this UI, not only ScrollView but also Editor for example. And ListView is available to do it, too. However, there are some problems...
  • In the case of using Editor, Command-Prompt-Like-UI is designed by adding prompt, command and command execution result as text data one after another to the Editor. The command input can be detected as TextChanged events, and the return key detection means command input completion. So, after the return key is pressed, identify the inputted command, display the command execution result, and make into command waiting mode by displaying the next new command prompt. I implemented this process, but the return key detection seems to fail and command text couldn't be extracted correctly. There is also a problem that prompt text could be deleted by backspace key as well as command text. And also, in Editor, it's impossible to change text decoration such as text color.
  • Adding prompt and command to ListView as list items also make it possible to build Command-Prompt-Like-UI. But display speed of ListView seems to be quite slow, so it's so hard to enable light scrolling of command prompt. Also, after the list size become longer than the display area height, the ListView scroll should be controlled from code behind by ScrollTo method. ScrollTo method can scroll ListView so that list item referred as one of the arguments of this method are correctly displayed, but the scroll control fails if there are items with the same name in the list, and I couldn't find to avoid it.
Therefore, I chose ScrollView finally, but it also has a problem with its scroll control from code behind. The behavior of ScrollToAsync method seems to be a little bit strange. It's explained later.

Below the Main Console is debug area to monitor some variables while running this App. ScrollView "debugScrollView" on the left side can scroll display many variable values one after another. Three Labels on the right side can keep monitoring some specific variables. In this development, they were useful pretty much to check the size of "Console", the amount of scroll and so on.

BTW, hierarchy of StackLayouts might look a little bit redundant. It is for displaying borderlines of each area using Padding and Gray/White background color.

The code-behind is below.

MainPage.xaml.cs
using System;
using Xamarin.Forms;

namespace CommandPrompt
{
    public partial class MainPage : ContentPage
    {
        int NoL = 0;        // The number of all Console lines
        int NoC = 0;        // The number of Commands
        int NoR1 = 0;       // The number of result lines on main console
        int NoR2 = 0;       // The number of result lines on debugScrollView
        int height = 24;    // Height of command and result line


        public MainPage()
        {
            InitializeComponent();

            // Display the 1st Command Prompt
            CommandPrompt();
        }

                
        // Create and Display a new Command Prompt
        void CommandPrompt()
        {
            // Command Prompt
            string prompt = "Prompt "+NoC.ToString()+">";            

            // Command Prompt
            Label Prompt = new Label
            {
                Text = prompt
                FontSize=15
                TextColor=Color.Blue, 
                HeightRequest= height
            };

            // Entry for command input
            Entry Command = new Entry
            {
                FontSize=15
                HeightRequest = height
                HorizontalOptions = LayoutOptions.FillAndExpand, 
                Keyboard = Keyboard.Plain  // No Capitalization and Spellcheck
            };
            Command.Completed += CommandCompleted;

            // Button to delete Entry gray border line
            Button DeleteEntryBorder = new Button
            {
                HeightRequest = height,
                HorizontalOptions = LayoutOptions.FillAndExpand,                
                BorderColor = Color.White, BorderWidth = 2,
                BackgroundColor = Color.Transparent,
                InputTransparent = true
            };

            // Overwrite "DeleteEntryBorder" Button on "Command" Entry
            Grid CommandGrid = new Grid
            {
                HorizontalOptions=LayoutOptions.FillAndExpand,                  
                Children = { Command, DeleteEntryBorder }
            };

            // Line up command prompt and command line
            StackLayout PromptCommand = new StackLayout
            {
                Orientation = StackOrientation.Horizontal, 
                Spacing = 0
                Children = { Prompt, CommandGrid }
            };

            // Add a new Prompt and Command Entry to Console
            Console.Children.Add(PromptCommand);


            // Expand scrollView content to show the last command line
            // (Somehow, Added this line, ScrollView are correctly displayed)
            scrollView.Content.HeightRequest = (NoL + 1 + NoR1) * height + 100;

            // Display the values on Label Monitor
            Label1.Text = string.Format("scrollView: H={0}, Con.H={1}",
                                        scrollView.Height.ToString(), scrollView.Content.Height.ToString());
            Label2.Text = string.Format("NoL={0}, (NoL+1)*h={1}",
                                        NoL.ToString(), ((NoL+1)*height).ToString());
            Label3.Text = string.Format("(Con.H-100)/24={0}, Con.H={1}",
                                        ((Console.Height-100)/height).ToString(), Console.Height.ToString());
            
            // Scroll to the last command line and focus its Entry 
            scrollView.ScrollToAsync(0, (NoL + 1) * height - scrollView.Height, false);

            Command.Focus();
            NoR1 = 0;
        }


        // This method is called, when RETURN Key in "Command" Entry
        void CommandCompleted(object senderEventArgs args)
        {
            Entry commandLine = sender as Entry;
            
            // Display data on ListView
            DisplayDebugScrollView(commandLine.Text);

            // Execute the inputted command
            CommandExecute(commandLine.Text);

            // increment NoL and NoC
            NoL++;
            NoC++;

            // Create next Command Prompt
            CommandPrompt();
        }


        // Identify Command and Execute it
        void CommandExecute(string command)
        {
            // Get the first three characters of command
            string com = "";
            if(command == string.Empty)
            {
                return;     // No command input, Return Key only
            }
            else if(command.Length<3)
            {
                com = command;
            }
            else
            {
                com = command.Substring(03);
            }
            

            // Identify the command from the three characters
            switch (com)
            {
                case "cat":
                    DisplayResult("show file content");
                    NoL++; NoR1=1;
                    break;
                    
                case "cd ":
                    DisplayResult("change directory");
                    NoL++; NoR1=1;
                    break;

                case "ech":  //echo
                    DisplayResult("write text in file");
                    NoL++; NoR1=1;
                    break;

                case "ls ":
                case "ls":
                    DisplayResult("show files");
                    NoL++; NoR1=1;
                    break;

                case "mkd":  //mkdir
                    DisplayResult("make directory");
                    NoL++; NoR1=1;
                    break;

                case "pwd":
                    DisplayResult("show directory");
                    NoL++; NoR1=1;
                    break;

                case "rm ":
                    DisplayResult("delete file");
                    NoL++; NoR1=1;
                    break;

                case "rmd":  //rmdir
                    DisplayResult("delete directory");
                    NoL++; NoR1=1;
                    break;

                case "tou":  //touch
                    DisplayResult("create file");
                    NoL++; NoR1=1;
                    break;

                default:    //others
                    DisplayResult("command not found");
                    NoL++; NoR1=1;
                    break;
            }
        }



        //Display command result on Main Console
        void DisplayResult(string result)
        {
            // Label to show Result
            Label Result = new Label
            {
                Text = result,
                FontSize = 15,
                HeightRequest = height
            };
            Console.Children.Add(Result);
        }


        //Display results on debugScrollView
        void DisplayDebugScrollView(string result)
        {
            // Label to show Result
            Label Result = new Label
            {
                Text = result,
                FontSize = 15,
                HeightRequest = height
            };
            debugResultList.Children.Add(Result);

            // Expand resultScrollView content to show the last Result
            debugScrollView.Content.HeightRequest = (NoR2 + 1) * height + height;

            // Scroll to the last Result
            debugScrollView.ScrollToAsync(0, (NoR2 + 1) * height - debugScrollView.Height, false);

            NoR2++;
        }


        // Three Buttons for Debug (check values and action)
        void Button1(object senderEventArgs args)
        {
            scrollView.ScrollToAsync(01000false);
        }
        void Button2(object senderEventArgs args)
        {
            CommandPrompt();
        }
        void Button3(object senderEventArgs args)
        {
            scrollView.ScrollToAsync(0, (NoL + 1) * height - scrollView.Height, false);
        }
    }
}

The CommandPrompt method creates and display command lines on the Main Console. A command line is built up from a prompt part and a command-input part. Label "Prompt" displays prompt text at the beginning of all command lines, and it's just text 'Prompt' plus the number of commands in this development. The current directory name could be the prompt name, if it can be gotten, in next development. Entry is used for command input part as I mentioned above. Keyboard property of the Entry is "Plain" so as to disable capitalization and spellcheck against the inputted text (command). When the return key is detected, a Completed event is raised and the CommandCompleted method is called.
  As you know, Entry has border line around itself (at least in the case of iOS). The borderline is unnecessary in Command-Prompt-Like-UI, so it should be deleted or hidden. In order to hide the borderline, I have implemented tricky code. Create the Button control "DeleteEntryBorder" that is the same size as Entry "Command". It has a white and a little bit thick borderline, its background color is transparent, and set its InputTransparent property "true." And then, overwriting "DeleteEntryBorder" on Entry "Command" in Grid control "CommandGrid", so the borderline of "Command" is hidden by the borderline of "DeleteEntryBorder". Build StackLayout "PromptCommand" by horizontally setting Label "Prompt" and "CommandGrid", then add the "PromptCommand" to the main console "Console" as its children.
  Next, forcibly expanded the content size of ScrollView "scrollView". I have not been able to understand why this expansion is needed yet. Even if some children such as Label, Entry, Grid, and StackLayout, are added to StackLayout inside ScrollView, their content size seem not to change. Anyway, this compulsive expansion make the scroll of the main console properly.
  Finally, scroll "Console" by ScrollToAsync method so that the last added command prompt is correctly displayed, and make into command waiting mode by focusing the Entry "Command".

The CommandCompleted method is called when the return key is detected in the Entry "Command". After displaying the text of the inputted command on one of the debug area, "debugScrollView", call the CommandExecute method. The CommandExecute method identifies the command's type from its first three letters, and displayed the command's execution result on the Main Console. In this post, I have not implemented actual file and directory I/O yet, just the inputted command type is simply displayed. And call the CommandPrompt method and finish this command prompt processing.

Button1-3 methods are for debug as I mentioned.

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





コメント

このブログの人気の投稿

Get the Color Code from an Image Pixel

PCL Storage (1)

Optimal Rectangle Packing