Xamarin.Forms UI Controls - Porting our Mobile Stock Market App to the Xamarin Platform (Part 3)

In this third post of our on-going blog series, we’ll detail the data presentation logic used for our Mobile Stock Market App and why/how we used lists for a variety of usage scenarios.

If you are new to this series, please be sure to review our first and second posts for background information on this app.

NOTE: At present, DevExpress Xamarin.Forms UI controls ship as part of our Universal Subscription. We expect to release a Xamarin-only product subscription in our v19.2 release cycle.

Sort Watchlist

As you may recall, the Watchlist includes a list of ticker symbols tracked/followed by a user. Our design allows users to sort the contents of the Watchlist against ticker symbols or a stock’s profitability. As you would expect, the DevExpress Xamarin Data Grid allows users to sort records via the column header as pictured below:

Unfortunately, in our app design, the Watchlist page does not include grid column headers and so we must rely upon the grid’s API to sort records. The easiest way to implement sort selection is to use a built-in Xamarin method to invoke an Action Sheet:

Our Xamarin Data Grid allows you to easily specify the desired sort column/sort order and its direction. In our app, we used the following code to modify sort order/direction via an Action Sheet:

View Markup

<views:UpdatablePage.ToolbarItems> 
    <ToolbarItem IconImageSource="sorting" Clicked="OnSortClicked"/ 
</views:UpdatablePage.ToolbarItems> 
<dxg:DataGridView ItemsSource="{Binding Content}">    
    <dxg:DataGridView.Columns> 
        <dxg:TemplateColumn  
            x:Name="contentColumn"  
            SortIndex="0"> 
            <dxg:TemplateColumn.DisplayTemplate> 
                <DataTemplate> 
                    <views:SymbolRow/> 
                </DataTemplate> 
            </dxg:TemplateColumn.DisplayTemplate> 
        </dxg:TemplateColumn> 
    </dxg:DataGridView.Columns> 
    Other grid customization is here.
</dxg:DataGridView>

View Code Behind

const string SortTitle = "Sort by"; 
const string CancelAction = "Cancel"; 
const string NoneAction = "None"; 
const string ByTickerAction = "Ticker"; 
const string ByTickerDescendingAction = "Ticker (Descending)"; 
const string ByProfitabilityAction = "Profitability"; 
const string ByProfitabilityDescendingAction = "Profitability (Descending)"; 
 
async Task SortSymbols() { 
    string action = await DisplayActionSheet( 
        SortTitle,  
        CancelAction, 
        null,  
        NoneAction, ByTickerAction, ByTickerDescendingAction, ByProfitabilityAction, ByProfitabilityDescendingAction); 
    switch (action) { 
        case NoneAction: 
            contentColumn.SortOrder = ColumnSortOrder.None; 
            break; 
        case ByTickerAction: 
            contentColumn.FieldName = "Ticker"; 
            contentColumn.SortOrder = ColumnSortOrder.Ascending; 
            break; 
        case ByTickerDescendingAction: 
            contentColumn.FieldName = "Ticker"; 
            contentColumn.SortOrder = ColumnSortOrder.Descending; 
            break; 
        case ByProfitabilityAction: 
            contentColumn.FieldName = "Change"; 
            contentColumn.SortOrder = ColumnSortOrder.Ascending; 
            break; 
        case ByProfitabilityDescendingAction: 
            contentColumn.FieldName = "Change"; 
            contentColumn.SortOrder = ColumnSortOrder.Descending; 
            break; 
        case CancelAction: break; 
        default: 
            throw new Exception("Cannot handle the specified action."); 
    }
}

Though Action Sheets are fine, we believe better UI options exist for sort order/direction (when using our Xamarin Data Grid without column headers).

Here are a couple of alternatives to Action Lists:

  1. A dropdown panel that displays available sort options within the page. A radio button signifies the current sort operation.

  2. A chips panel that allows users to select the data field by which the rows should be sorted, and its order (ascending or descending).

Which of these three methods do you consider more intuitive and easier to use? Please share your thoughts with us in the comments section below.

Swipe Actions to Remove and Edit Transactions

The Transaction list page allows users to manage hypothetical portfolio transactions. Users can add a new transaction and edit/remove an existing transaction as needed. Since our goal is UI simplicity and clarity, we included transaction actions (edit and remove) for each row and allowed users to apply them via swipes.

Our Xamarin Data Grid allows us to attach these actions to individual rows with ease.

<dxg:DataGridView  
    FullSwipeMode="None" 
    ItemsSource="{Binding Rows}"> 
    <dxg:DataGridView.StartSwipeItems> 
        <dxg:SwipeItem Width="95" Command="{Binding EditTransactionCommand}"> 
            <dxg:SwipeItem.Template> 
                <DataTemplate>  
                    <views:EditSwipeButton/> 
                </DataTemplate> 
            </dxg:SwipeItem.Template> 
        </dxg:SwipeItem> 
    </dxg:DataGridView.StartSwipeItems> 
    <dxg:DataGridView.EndSwipeItems> 
        <dxg:SwipeItem Width="95" Command="{Binding RemoveTransactionCommand}"> 
            <dxg:SwipeItem.Template> 
                <DataTemplate>  
                    <views:DeleteSwipeButton/> 
                </DataTemplate> 
            </dxg:SwipeItem.Template> 
        </dxg:SwipeItem> 
    </dxg:DataGridView.EndSwipeItems> 
    Other grid customization is here.
<dxg:DataGridView> 

Group Symbols in the Market Conditions Page

The Market Conditions Page displays trending stock market symbols: Most Active, Gainers, and Losers.

The DataGridView offers various column types that we can use to display content and to group data. The code below uses our PickerColumn to provide values against which the grid will group rows and our TemplateColumn to display row content:

<dxg:DataGridView 
    ItemsSource="{Binding Content}" 
    AllowGroupCollapse="True"> 
    <dxg:DataGridView.Columns> 
        <dxg:PickerColumn 
            FieldName="ListType" 
            IsGrouped="True"/> 
        <dxg:TemplateColumn> 
            <dxg:TemplateColumn.DisplayTemplate> 
                <DataTemplate> 
                    <views:SymbolRow/> 
                </DataTemplate> 
            </dxg:TemplateColumn.DisplayTemplate> 
        </dxg:TemplateColumn> 
    </dxg:DataGridView.Columns> 
    <dxg:DataGridView.GroupRowTemplate> 
        <DataTemplate> 
            <views:SymbolGroupRow  
                Text="{Binding Value, Converter={converters:ListTypeToListCaptionConverter}}" 
                GroupImageSource="{Binding Text, Source={x:Reference groupRow}, Converter={converters:ToFilenameConverter}}"/> 
        </DataTemplate> 
    </dxg:DataGridView.GroupRowTemplate> 
    Other grid customization is here.
</dxg:DataGridView> 

Update Row on Data Object Change within the Search Page

The Search Symbol page is invoked from the app’s Watchlist. It allows users to populate watchlists with desired stock symbols. All symbols found by the typed ticker fragment are divided into two groups: symbols that are in the watchlist, and symbols that are not in the watchlist. A symbol is moved from one group to another via a row click event.

The IsInWatchlist property specifies whether a symbol is in a watchlist. The Data Grid must handle these property changes and move the item to the appropriate group. The grid can handle object changes when an item’s source implements the IBindingList interface. Note that data objects also should implement the INotifyPropertyChanged interface.

View Markup

<dxg:DataGridView 
    ItemsSource="{Binding Content}" 
    SortMode="Multiple"> 
    <dxg:DataGridView.GroupRowTemplate> 
        <DataTemplate> 
            <views:SymbolGroupRow  
                Text="{Binding Value, Converter={converters:BoolToStringConverter TrueString='IN WATCHLIST', FalseString='ADD TO WATCHLIST'}}"/> 
        </DataTemplate> 
    </dxg:DataGridView.GroupRowTemplate> 
    <dxg:DataGridView.Columns> 
        <dxg:CheckBoxColumn 
            FieldName="IsInWatchlist" 
            IsGrouped="True" 
            SortIndex="1" 
            SortOrder="Descending"> 
        </dxg:CheckBoxColumn> 
        <dxg:TemplateColumn 
            FieldName="Ticker" 
            SortIndex="2" 
            SortOrder="Ascending"> 
            <dxg:TemplateColumn.DisplayTemplate> 
                <DataTemplate> 
                    <views:WatchListSymbolSearchRow/> 
                </DataTemplate> 
            </dxg:TemplateColumn.DisplayTemplate> 
        </dxg:TemplateColumn> 
    </dxg:DataGridView.Columns> 
</dxg:DataGridView> 

View Model Code

public class SearchListSymbolItem: ListSymbolWrapper { 
    bool isInWatchlist; 
 
    public bool IsInWatchlist { 
        get => isInWatchlist; 
        set => SetProperty(ref isInWatchlist, value); 
    } 
    public ICommand OpenSymbolDetailCommand { get; } 
 
    public SearchListSymbolItem( 
        ListSymbol listSymbol,  
        bool isInWatchlist = false, 
        Action<ListSymbol> openSymbolDetailHandler = null 
    ) : base(listSymbol) { 
        IsInWatchlist = isInWatchlist; 
        OpenSymbolDetailCommand = new DelegateCommand(() => openSymbolDetailHandler?.Invoke(listSymbol)); 
    } 
} 
 
public abstract class ItemSearchViewModel<T> : NotificationObject { 
    string fragment; 
    BindingList<T> content; 
 
    public string Fragment { 
        get => fragment; 
        set => SetProperty(ref fragment, value, onChanged: OnFragmentChanged); 
    } 
    public BindingList<T> Content {  
        get => content; 
        set => SetProperty(ref content, value); 
    } 
 
    // The parent calls this method when a user finises typing. 
    protected void LoadContent() { 
        LoadContentAsync().InvokeAndForgetSafeAsync(this); 
    } 
    async Task LoadContentAsync() { 
        BindingList<T> symbols = await FindSymbolsAsync(); 
        await Device.InvokeOnMainThreadAsync(() => { Content = symbols; }); 
    } 
    // Other members are here. 
} 
 
public class WatchListSymbolSearchViewModel : ItemSearchViewModel<SearchListSymbolItem> { 
    // The parent calls this method when a user finises typing. 
    protected async override Task<BindingList<SearchListSymbolItem>> FindSymbolsAsync() { 
        IEnumerable<ListSymbol> symbols = await this.ListSymbolRepository.FindByFragment(this.Fragment); 
        List<SearchListSymbolItem> symbolItems = symbols 
            .Select(s => new SearchListSymbolItem(s, IsTickerInWatchList(s.Ticker), OpenSymbolDetail)) 
            .ToList(); 
        return new BindingList<SearchListSymbolItem>(symbolItems); 
    } 
    // Other members are here. 
} 

Shared Scrolling within the Portfolio Page

One of our most interesting uses of the DevExpress Xamarin Grid is within the Portfolio page. In it, the Grid View shares scrolling with its container - the Scroll view.

We had to manually calculate the DataGridView height to implement the behavior pictured above:

View Markup

<ScrollView> 
    <Grid> 
        <dxg:DataGridView 
            Grid.Row="2" 
            HeightRequest="{Binding RowsHeight}" 
            ItemsSource="{Binding Rows}" 
            RowHeight="{Binding RowHeight}" 
            ColumnHeaderHeight="{Binding HeaderHeight}"> 
            Other data grid customizations are here.
        </dxg:DataGridView> 
        Other layout customizations are here.
    </Grid> 
</ScrollView>

View Model Code

public class PortfolioViewModel : BaseViewModel, IErrorHandler { 
    ICollection<PortfolioItemViewModel> rows; 
 
    public ICollection<PortfolioItemViewModel> Rows { 
        get => rows; 
        set => SetProperty(ref rows, value, onChanged: (_, v) => RaisePropertyChanged(nameof(RowsHeight))); 
    } 
    public double RowHeight => 95; 
    public double HeaderHeight => 25; 
    public double RowsHeight => (rows != null) ? HeaderHeight + RowHeight * rows.Count : HeaderHeight; 
 
    // Other members go there. 
} 

What’s Next

With list related tasks complete, we’ll move our focus to charting and demonstrate how to leverage our Xamarin UI Charting library to deliver information rich analytics to your end-users.

Your Feedback Matters

As always, if you’ve yet to try our Xamarin.Forms UI controls or would like to discuss your business needs, please comment below or write to us at info@devexpress.com.

Free DevExpress Products - Get Your Copy Today

The following free DevExpress product offers remain available. Should you have any questions about the free offers below, please submit a ticket via the DevExpress Support Center at your convenience. We'll be happy to follow-up.
No Comments

Please login or register to post comments.