Smart Office SDK – First Project Part 6 – the Browse Control

This is the 6th in a long series of posts of using the Smart Office SDK to remove modifications – yes I am still on the first project.

In this post I’ll be discussing the Browse Control and creating our own equivalent functionality.

 


So, in my Vessel Modification I have quite a number of fields that the user can click on to select entries. In the screenshot above we are browsing the list of vessels and voyages and we have the pretty M3 Browse option.

Now I wanted to reproduce the functionality and keep it looking as close to this as possible. I also didn’t want to have to build a separate browse for each of the different types of fields – which though would have been a lot easier, would have made life unbearable in the future when it comes to maintaining.

This of-course poses a problem when we have different types of data, variable numbers of columns and we’ll throw in filtering for a laugh too.

So I had a look around and I couldn’t really see a standard M3 way for me to take advantage of the existing functionality – it may be there, but well…

So what we end up with is this:

…which though isn’t the same, it provides a very similar experience with less of those confusing buttons.

The list is in a Datagrid control, and each of the columns that have a textbox in the title allow for filtering. We then have the Select and Cancel buttons.

In my code I actually only retrieve the vessels that have a sailing date >= 1 month prior to todays date (I’ve disabled in the screenshot to provide a bit of a better comparison to the modification). And we can scroll <gasp> down our list in a consistent fashion too!

Given the volumes of data that we process, there isn’t really any need for us to handle loading records in chunks.

Styling

Probably the most challenging thing in this was to determine what styling we needed to use.

With the DataGrid the XAML I am using is as follows

<DataGrid Margin="10,10,105,40" ColumnWidth="SizeToCells" Name="lvItems" AutoGenerateColumns="False" Style="{StaticResource styleDataGrid}" SelectionMode="Single" PreviewMouseLeftButtonDown="lvItems_PreviewMouseLeftButtonDown" GridLinesVisibility="None" HeadersVisibility="Column"  />
																													

 

The styleDataGrid was rather elusive – I just couldn’t see it for a while and ended up with a poxy inconsistent experience.

We don’t want columns auto-generated as we want to give them friendly names – we are also going to have a lot more data in the collection we add to the ItemsSource than we want or need to display.

The HeadersVisibility set to Column means we don’t get an ugly pseudo column down the left.

GridLinesVisibility set to none is fairly obvious, but I raise this there is a setting within the Client settings about whether or not gridlines should be displayed. If we were good little developers then I’d retrieve that value and set the gridlines appropriately – but I guess this is why we have multiple versions of things 🙂

Adding Columns

So, the heart of the part of the project is the AddColumn() function – because we don’t know what data we will be adding, the binding characteristics or really any useful information, we have to create the relationships on the fly.

AddColumn will set the column name, the binding to the data in the object we add to ItemsSource, the default width of the column and if it is a filter column.

        /// <summary>

        /// add a new column to our Datagrid and set the binding

        /// </summary>

        /// <param name=”astrColumnName”>this is the name that should be displayed in the header</param>

        /// <param name=”astrBindName”>this is the property name that we will be binding the column to</param>

        /// <param name=”adblWdith”>the minimum width, if filter is set to true we add an extra 17 pixels to this width</param>

        /// <param name=”abFilter”>should we be able to filter on this column?</param>

        /// <returns>the newly created column</returns>

        public DataGridTextColumn AddColumn(string astrColumnName, string astrBindName, double adblWdith, bool abFilter)

 

It will create a new DataGridTextColumn and add a stackpanel to the headers content. We then add a TextBlock with the astrColumnName value. We check to see if the abFilter is set to true, if yes, then we will add a TextBox to our StackPanel.

Then we need to initialise an couple of internal arrays which store the TextBox for the filtered column and an array of strings which store the binding name which we use for easier filtering later.

When I was working on another part of this project which involved a DataGrid I ended up taking this code and refining it further so it doesn’t create the arrays but has a list of an object that contains the TextBox, binding name and some other information to provide some extra flexibility and wraps it in a pluggable new usercontrol that can just be ‘dropped’ in – I haven’t pushed that code back in to the Browse yet but aim to before I go live.

When then actually set the header, the binding and the minimum width of our column before finally added the created column to our datagrid.

Presenting to the User

Because we are fairly dynamic we can end up with things not lining up properly in the header due to filters. Lets say the first column we don’t have a filter TextBox and then the second we do. The first column title may not line up nicely.

So to get around this we use the Grid_Loaded event and we

a). set up the filtering (this includes setting up events to monitor for changes to the filter TextBoxes
b). add another TextBlock to the columns without filters. If there are no filters what-so-ever we don’t bother

Again, in the other part of the project where I created a DataGrid with all of this functionality, I needed to change this logic a little as we couldn’t rely upon the Grid_Loaded event. But more on that in another post.

Applying the Filters

During the set up of the filters, we subscribe to the keydown event on the filter TextBoxes – perhaps the keyup would be the smart choice, but meh.

Specifically we look for the user pressing the enter key. If they press that we will apply our filters using some very generic code.

In my enhanced DataGrid I added code which would allow the filtering >= to number

Closing the Browse Window

Because we are using a Smart Office-ified browse, then we need to handle the closing of our window, for that when we are called we need to pass our IInstanceHost object to our Browse Window. We will then use the .Close() to clean up.

Idiosyncrasies

I did have some issues around the double clicking to select an item initially, this resulted in me subscribing to the PreviewMouseLeftButtonDown event and filtering the events. If I didn’t do that it was causing some nasty exceptions. I actually think I had this many years ago when using the WPFToolkits Datagrid but wasn’t too hard to work around with some code shamelessly pilfered off the net.

The XAML

<UserControl x:Class="VesselMod.BrowseControl"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:Controls="clr-namespace:Mango.UI.Controls;assembly=Mango.UI" 
        xmlns:log4net="clr-namespace:log4net;assembly=log4net"
        xmlns:mangocore="clr-namespace:Mango.Core;assembly=Mango.Core"
        xmlns:mangouicore="clr-namespace:Mango.UI.Core;assembly=Mango.UI"    
        xmlns:mashup="clr-namespace:Mango.UI.Services.Mashup;assembly=Mango.UI"
        MinHeight="351" MinWidth="526">
    <Grid Loaded="Grid_Loaded">
        <DataGrid Margin="10,10,105,40" ColumnWidth="SizeToCells" Name="lvItems" AutoGenerateColumns="False" Style="{StaticResource styleDataGrid}" SelectionMode="Single" PreviewMouseLeftButtonDown="lvItems_PreviewMouseLeftButtonDown" GridLinesVisibility="None" HeadersVisibility="Column"  />

        <!--<ListView Margin="10,10,105,40"  Style="{StaticResource styleListView}" Name="lvItems">
            <ListView.View>
                <GridView ColumnHeaderContainerStyle="{StaticResource styleGridViewColumnHeader}">
                </GridView>
            </ListView.View>
            <DataGrid Height="100" Width="100"/>
        </ListView>-->
        <Controls:StatusBar Name="stsStatusBar" VerticalAlignment="Bottom" />
        <Button x:Name="btnSelect" Content="Select" HorizontalAlignment="Right" Margin="0,0,10,82" VerticalAlignment="Bottom" Width="90" IsDefault="True" Click="btnSelect_Click" Style="{DynamicResource styleButtonPrimary}"/>
        <Button x:Name="btnCancel" Content="Cancel" HorizontalAlignment="Right" Margin="0,0,10,53" VerticalAlignment="Bottom" Width="90" IsCancel="True"/>
    </Grid>

</UserControl>

 

The Code

I’m in two minds about posting the code, I think it is useful. Just bear in mind that this is around providing ideas on how I am approaching the situation, it’s by no means finished, definitive, or even the correct way. 🙂

using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

using System.ComponentModel;

using Mango.Services;

namespace VesselMod
{
    /// <summary>
    /// Interaction logic for BrowseControl.xaml
    /// </summary>
    public partial class BrowseControl : UserControl
    {
        private TextBox[] _filteredcolumns = null;
        public TextBox[] FilteredColumns
        { 
            get { return _filteredcolumns; }
            set { _filteredcolumns = value; }
        }

        private string[] _filteredfields = null;
        /// <summary>
        /// these are the fields that we will search when filtering
        /// they should be in the same order as the textboxes in the FilteredColumns
        /// </summary>
        public string[] FilteredFields 
        {
            get { return _filteredfields; }
            set { _filteredfields = value; }
        }

        System.Reflection.PropertyInfo[] propertyInfo { get; set; }
        Delegate[] propertyResolver { get; set; }

        object objOriginalRow = null;

        /// <summary>
        /// this is the item selected from our list
        /// </summary>
        public object SelectedItem { get; set; }

        public IInstanceHost Host { get; set; }

        public BrowseControl()
        {
            InitializeComponent();

        }

        /// <summary>
        /// if we have filtering in place, with columns that aren't
        /// filtered, then we add a textblock to the stack panel of
        /// the columns that don't have filtering to make the column
        /// title line up
        /// </summary>
        private void addHeightToColumnsIfFiltersPresent()
        {
            if ((null != FilteredColumns) && (Enumerable.Count(FilteredColumns) >= 1))
            {
                foreach (DataGridColumn dgcCurrent in lvItems.Columns)
                {
                    if (null != dgcCurrent)
                    {
                        StackPanel spCurrent = dgcCurrent.Header as StackPanel;
                        if (null != spCurrent)
                        {
                            if (spCurrent.Children.Count == 1)
                            {
                                spCurrent.Children.Add(new TextBlock());
                            }
                        }
                    }
                }
            }
        }

        public void SetFilters()
        {
            if((null != FilteredColumns) && (Enumerable.Count(FilteredColumns) >= 1) && (lvItems.ItemsSource != null) && (Enumerable.Count((IEnumerable<object>)lvItems.ItemsSource) >= 1))
            {
                propertyInfo = new System.Reflection.PropertyInfo[FilteredColumns.Length];
                // propertyResolver = new Delegate[FilteredColumns.Length];
                for (int i = 0; i < FilteredColumns.Length; i++)
                {
                    TextBox currentTextBox = FilteredColumns[i];
                    if (null != currentTextBox)
                    {
                        currentTextBox.KeyDown += currentTextBox_KeyDown;
                    }
                    propertyInfo[i] = ((IEnumerable<object>)lvItems.ItemsSource).First().GetType().GetProperty(FilteredFields[i]);
                }
            }
        }

        void currentTextBox_KeyDown(object sender, KeyEventArgs e)
        {
            if (e.Key == Key.Enter)
            {
                DateTime dtStart = DateTime.Now;
                ApplyFilters();
                TimeSpan tsTime = DateTime.Now - dtStart;

                System.Diagnostics.Debug.WriteLine("Filter took:" + tsTime.TotalMilliseconds);
                e.Handled = true;
            }
        }

        private void lvItems_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            DataGridRow dgRow = GetVisualParentByType((FrameworkElement)e.OriginalSource, typeof(DataGridRow)) as DataGridRow;

            if ((e.ClickCount >= 2) && (null != lvItems.SelectedItem) && (null != dgRow))
            {
                btnSelect_Click(null, null);
            }
            else
            // handle a bug that causes ISO to crash when we click on the same cell twice
                if ((null != objOriginalRow) && (true == object.ReferenceEquals(dgRow, objOriginalRow)))
            {
                e.Handled = true;
            }

            objOriginalRow = dgRow;
        }

        private void btnSelect_Click(object sender, RoutedEventArgs e)
        {
            SelectedItem = lvItems.SelectedItem;
            Host.Close();
        }


        public void ApplyFilters()
        {
            ICollectionView view = CollectionViewSource.GetDefaultView(lvItems.ItemsSource);
            if (view != null)
            {
                view.Filter = FilterPredicate;
            }
        }

        private bool FilterPredicate(object item)
        {
            bool result = true;
            for (int i = 0; i < FilteredColumns.Length; i++)
            {
                if(false == string.IsNullOrEmpty(FilteredColumns[i].Text))
                {
                    string strValue = propertyInfo[i].GetValue(item, null).ToString();

                    if (false == string.IsNullOrEmpty(strValue))
                    {
                        if (false == (result = strValue.StartsWith(FilteredColumns[i].Text)))
                        {
                            break;
                        }
                    }
                }
            }
            return result;
        }

        /// <summary>
        /// add a new column to our Datagrid and set the binding
        /// </summary>
        /// <param name="astrColumnName">this is the name that should be displayed in the header</param>
        /// <param name="astrBindName">this is the property name that we will be binding the column to</param>
        /// <param name="adblWdith">the minimum width, if filter is set to true we add an extra 17 pixels to this width</param>
        /// <param name="abFilter">should we be able to filter on this column?</param>
        /// <returns>the newly created column</returns>
        public DataGridTextColumn AddColumn(string astrColumnName, string astrBindName, double adblWdith, bool abFilter)
        {
            DataGridTextColumn result = new DataGridTextColumn();

            StackPanel stackpanelBrowse = new StackPanel();
            stackpanelBrowse.Orientation = Orientation.Vertical;
            stackpanelBrowse.VerticalAlignment = System.Windows.VerticalAlignment.Top;
            stackpanelBrowse.Children.Add(new TextBlock() { Text = astrColumnName, Padding = new Thickness(1), VerticalAlignment = System.Windows.VerticalAlignment.Top });
            
            if (true == abFilter)
            {
                stackpanelBrowse.Children.Add(new TextBox() { VerticalAlignment = System.Windows.VerticalAlignment.Bottom, HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch, Width = adblWdith });

                if (null == FilteredColumns)
                {
                    FilteredColumns = new TextBox[1];
                    FilteredFields = new string[1];
                }
                else
                {
                    Array.Resize(ref _filteredcolumns, (FilteredColumns.Length + 1));
                    Array.Resize(ref _filteredfields, (FilteredFields.Length + 1));
                }

                FilteredColumns[FilteredColumns.GetUpperBound(0)] = (TextBox)stackpanelBrowse.Children[1];
                FilteredFields[FilteredFields.GetUpperBound(0)] = astrBindName;
            }

            result.Header = stackpanelBrowse;
            result.Binding = new Binding(astrBindName);
            result.MinWidth = adblWdith + 17;

            lvItems.Columns.Add(result);

            return (result);
        }

        public static DependencyObject GetVisualParentByType(DependencyObject startObject, Type type)
        {
            DependencyObject parent = startObject;
            while (parent != null)
            {
                if (type.IsInstanceOfType(parent))
                    break;
                else
                    parent = VisualTreeHelper.GetParent(parent);
            }

            return parent;
        }

        private void Grid_Loaded(object sender, RoutedEventArgs e)
        {
            SetFilters();
            addHeightToColumnsIfFiltersPresent();
        }
    }
}

 

This entry was posted in Development, M3 / MoveX, SDK. Bookmark the permalink.

1 Response to Smart Office SDK – First Project Part 6 – the Browse Control

  1. Pingback: Browse Control for ISO SDK App–Part 1 – Infor M3 Talks

Leave a comment