LSO Scrolling through Lists & HasMoreRows

In my last post I write about being able to programmatically scroll through an M3 list, you may want to do this for several reasons – such as populating the ListView so you can enumerate through its entire contents and not just the records that were initially retrieved.

I vaguely mentioned that the reason I was looking in to it was due to some very specific requirements – we have a mod in PMS050 which makes manufacturing reporting for ‘normal’ staff challenging. Closing a MO ends up being done through PMS050 panel B. Panel B doesn’t have much in the way of filtering.

As a temporary work around I decided that I would add a TextBox for an product number (ITNO) and a button which would initiate a script that would look for the Textbox value in the first column of the ListView, if ITNO wasn’t found, it will simulate two page downs and then check to see if it can find ITNO – this process will be repeated until it finds ITNO or we reach the end of the list.

There were a number of challenges around this:

  • LSO will retrieve a small sample of records and display them. As you start scrolling down a ListView it will retrieve another block of records to be displayed – we never know exactly how many records make up the ListView. The ListView itself has no idea either. This makes determining if we have reached the bottom of the list challenging.
  • When LSO requests its next set of records it takes time, during my testing my loop would complete up to 4 times before the new records were retrieved and added to the ListView – so I couldn’t compare a previous record count (ListView.Items.Count) against the record count after the page down.
  • How do you simulate the key depression and what are the ramifications?

Addressing point 1, this took a while to resolve until I caved and investigated the LSO UI assembly. Smart Office wraps a WPF ListView in the ListControl object, and one of the properties that the ListControl exposes is HasMoreRows. HasMoreRows is Boolean and will be true as long as there are more records to retrieve. Once we have hit the last it will be false.

Point 2, once I discovered this variable, issue two just disappeared 🙂

Point 3, well, that’s discussed in my previous post, however this did have some unforeseen issues. The usage of SendKeys from the Windows.Forms assembly will send the keys to the currently active Window and control. If you open another Window then it will take the focus and of-course will start receiving the SendKeys events – not so much of an issue for most situations but it is a problem nonetheless. I did take a look at creating a KeyEventArgs object and using the RaiseEvent of the ListControl which I would expect to get around the issue but I couldn’t get it to consistently work – but that’s why we have versions of software 🙂

Below is a snap of the final product.

Now with that over, here’s the code that I will be distributing to my user.

 

// Name: PMS050_B_SendKey
// Version: 1.00 2010-12-02
// Description: adds a TextBox and Button and will scroll
//	through the listview until it finds a matching product
//
// Notes: Simulates key depressions for PageDown, this 
//	has the undesired effect of working on the currently
//	active window.
//	We use an undocumented feature in the ListControl called
//	HasMoreRows to determine when we should stop scrolling
//	* Using KeyEventArgs should be investigated further
//		var k :KeyEventArgs = new KeyEventArgs(Keyboard.PrimaryDevice, Keyboard.PrimaryDevice.ActiveSource, 0, Key.Next);
//		k.RoutedEvent = Keyboard.KeyDownEvent;
//		listControl.ListView.RaiseEvent(k);
//	* Determine a method to break the loop in a friendly fashion

import System;
import System.Windows;
import System.Windows.Controls;
// used for the SendWait
import System.Windows.Forms;
import MForms;

package MForms.JScript {
   class PMS050_B_SendKey {
   		var gdebug, gbtnGotoItem, gcontroller, gtbTextBox;
   		var gbRunning = false;
   		var giLVItemCount = 0;
   		var gbtnStop;
   		
		public function Init(element: Object, args: Object, controller : Object, debug : Object)
		{
			try
			{
				// save these aganist the class so we
				// can use them in other functions
				gcontroller = controller;
				gdebug = debug;

				// create the TextBox that contains the
				// ITNO
				gtbTextBox = new TextBox();
				gtbTextBox.Width = 50;
				
				// create the Button that will
				// initiate the search
				gbtnGotoItem = new Button();
				gbtnGotoItem.Content = "Go to Item";
				Grid.SetColumnSpan(gbtnGotoItem, 10);
				Grid.SetColumn(gbtnGotoItem, 11);
				Grid.SetRow(gbtnGotoItem, 0);

				gbtnStop = new Button();
				gbtnStop.Content = "Stop";

				Grid.SetColumnSpan(gbtnStop, 10);
				Grid.SetColumn(gbtnStop, 19);
				Grid.SetRow(gbtnStop, 0);

				Grid.SetColumnSpan(gtbTextBox,12);
				Grid.SetColumn(gtbTextBox, 0);
				Grid.SetRow(gtbTextBox, 0);
				
				// Add the controls
				controller.RenderEngine.Content.Children.Add(gtbTextBox);								
				controller.RenderEngine.Content.Children.Add(gbtnGotoItem);
				//controller.RenderEngine.Content.Children.Add(gbtnStop);
				
				gbtnGotoItem.add_Click(OnClick);
				gbtnGotoItem.add_Unloaded(OnUnloaded);
				gbtnStop.add_Click();
				gbtnStop.add_Unloaded();
			}
			catch(ex)
			{
				debug.WriteLine(ex.Message);
			}
      }

		// check to make sure that the ListControl currently has
		// focus, if yes, then we will issue the pagedown sendkey
		private function pageDown(alcListControl)
		{
			try
			{
				// ensure that the ListControl has focus
				alcListControl.Focus();
				// 'press' the Page Down key
				System.Windows.Forms.SendKeys.SendWait("{PGDN}");
				System.Windows.Forms.SendKeys.SendWait("{PGDN}");
			}
			catch(ex)
			{
				//MessageBox.Show(ex.Message);
				gdebug.WriteLine(ex.Message + " " + ex.StackTrace);
			}
		}
		
		// searchForSpecificITNO
		//  scroll down through the alvListControl until we match our ITNO
		// alvListControl = the ListView that we want to interate through
		// astrITNO = the Item Number (we actually do a IndexOf when we compare)
		// aiColumnNumber = the column that we should look for the astrITNO in
		private function searchForSpecificITNO(alvListControl, astrITNO, aiColumnNumber)
		{
			try
			{
				if(!String.IsNullOrEmpty(astrITNO))
				{
					var lvListView = alvListControl.ListView;
					// set the focus to the ListView
					alvListControl.Focus();

					// start off with a pagedown, the user probably
					// won't do a search if they can already see it
					// on the screen
					pageDown(alvListControl);

					// have we found the ITNO?
					var bFound = false;
					
					// we need 3 conditions to be true before we exit this while loop
					//  1). has the ITNO been found?
					//  2). is there more rows to be retrieved?
					//  3). should we continue running - ideally we want to add a method to cancel the looping in a future version
					while((false == bFound) && (true == alvListControl.HasMoreRows) && (true == gbRunning))
					{
						// loop through the current items in the list, we want to make sure that we
						// start at the last value so we don't search the same items multiple times
						for(var iCount : int = giLVItemCount; iCount < lvListView.Items.Count; iCount++)
						{
							// retrieve the current record
							var itmCurrentItem = lvListView.Items[iCount];
							if(null != itmCurrentItem)
							{
								// ensure that there the column we are searching for isn't
								// null on the current record
								if(!String.IsNullOrEmpty(itmCurrentItem[aiColumnNumber]))
								{
									// convert the current item to a string
									var strCurrentString = itmCurrentItem[aiColumnNumber].ToString();
									
									// we only care about a partial match
									if(-1 != strCurrentString.IndexOf(astrITNO))
									{
										// because we are nice, we'll scroll the first matching
										// item in to view
										lvListView.ScrollIntoView(itmCurrentItem);
										lvListView.SelectedItem = itmCurrentItem;
										bFound = true;
										break;
									}
								}
							}
						}
						if(false == bFound)
						{
							// we didn't find the item, we want to make note
							// of the previous item count (I was using this
							// originally so I knew when to stop, however the loop
							// would complete before M3 had returned the new records :-O
							giLVItemCount = lvListView.Items.Count;

							// pagedown twice!
							pageDown(alvListControl);
							pageDown(alvListControl);

						}
						else
						{
							break;
						}
					}
				}
			}
			catch(ex)
			{
				gdebug.WriteLine(ex.Message);
			}
		}

		public function OnStopClick(sender: Object, e: RoutedEventArgs)
		{
			gbRunning = false;
		}

		public function OnClick(sender: Object, e: RoutedEventArgs)
		{
			gbRunning = true;	// this is what we require for our loop
			try
			{
				if(null != gcontroller)
				{
					// retrieve the ListView
					var listControl = gcontroller.RenderEngine.ListControl;
					gdebug.WriteLine("listControl");
					// set the focus to the ListView
					listControl.Focus();
					gdebug.WriteLine("listControl.Focus()");
					searchForSpecificITNO(listControl, gtbTextBox.Text, 0);
	
				}
				else
				{
					MessageBox.Show("no controller");
				}				
			}
			catch(ex)
			{
				gdebug.WriteLine(ex.Message);
			}
		}

		public function OnStopUnloaded(sender: Object, e: RoutedEventArgs)
		{
			gbRunning = false;
			gbtnStop.remove_Click(OnStopClick);
			gbtnStop.remove_Unloaded(OnStopUnloaded);
		}

		public function OnUnloaded(sender: Object, e: RoutedEventArgs)
		{
			gbtnGotoItem.remove_Click(OnClick);
			gbtnGotoItem.remove_Unloaded(OnUnloaded);
		}

   }
}

This entry was posted in Development, How Far is Too Far?, M3 / MoveX. Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s