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); } } }