· More information on Telerik Wpf controls (and a trial) is available here: [http://www.telerik.com/products/wpf/controls.aspx]
· Creating a Wpf UserControl means that I can re-use the “Client Inception” module in the Dynamics rich-client as well as Sharepoint EP and any other windows based application I choose.
· Note; a zero-value on the slider does not necessary mean an “exact-string-match”. It means that the search term must resolve to the same Soundex code.
Xaml |
<UserControl x:Class="ClientInceptionUserControl.Startup" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:telerik="http://schemas.telerik.com/2008/xaml/presentation" mc:Ignorable="d" d:DesignHeight="500" d:DesignWidth="800"> <Grid> <telerik:RadPanelBar Margin="10" Width="Auto" Height="Auto" Orientation="Vertical" ExpandMode="Multiple" telerik:StyleManager.Theme="Office_Blue" > <telerik:RadPanelBarItem Header="Search" VerticalAlignment="Top" IsExpanded="True"> <Grid Height="70" Background="White"> <Grid.RowDefinitions> <RowDefinition Height="5" /> <RowDefinition Height="30" /> <RowDefinition Height="30" /> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto" /> <ColumnDefinition Width="200" /> <ColumnDefinition Width="20" /> <ColumnDefinition Width="Auto" /> <ColumnDefinition Width="200" /> <ColumnDefinition Width="20" /> <ColumnDefinition Width="Auto" /> </Grid.ColumnDefinitions> <Label Grid.Row="1" Grid.Column="0" Content="Tolerance:"/> <Label Grid.Row="1" Grid.Column="3" Content="Tolerance:"/> <Label Grid.Row="2" Grid.Column="0" Content="Search name:"/> <Label Grid.Row="2" Grid.Column="3" Content="Search address:"/> <TextBox x:Name="SearchName" Grid.Row="2" Grid.Column="1" Margin="3" /> <TextBox x:Name="SearchAddress" Grid.Row="2" Grid.Column="4" Margin="3" /> <telerik:RadSlider x:Name="SearchNameTolerance" Grid.Row="1" Grid.Column="1" VerticalAlignment="Center" Maximum="100" Value="30" IsSnapToTickEnabled="True" TickFrequency="10" Margin="10 0" TickPlacement="Both" /> <telerik:RadSlider x:Name="SearchAddressTolerance" Grid.Row="1" Grid.Column="4" VerticalAlignment="Center" Maximum="100" Value="10" IsSnapToTickEnabled="True" TickFrequency="10" Margin="10 0" TickPlacement="Both" /> <telerik:RadButton x:Name="ProximitySearch" Grid.Row="2" Grid.Column="6" Width="100" Content="Search" Click="ProximitySearch_Click" CornerRadius="20" Height="25"></telerik:RadButton> </Grid> <telerik:RadGridView x:Name="SearchGrid" AutoGenerateColumns="True" IsReadOnly="True" /> </telerik:RadPanelBarItem> <telerik:RadPanelBarItem Header="Details"> <telerik:RadPanelBarItem Header="..." /> </telerik:RadPanelBarItem> </telerik:RadPanelBar> </Grid> </UserControl> |
C# |
privatevoid ProximitySearch_Click(object sender, RoutedEventArgs e) { // ensure that both search terms have been entered if ((SearchName.Text == "") || (SearchAddress.Text == "")) { MessageBox.Show("Please supply a word from the name and the address of the new entity.","Client inception"); return; } string searchNameSoundexCode = SoundexClass.createSoundexCodeEN(SearchName.Text); string searchAddressSoundexCode = SoundexClass.createSoundexCodeEN(SearchAddress.Text); // ensure that a soundex can be generated for both terms if ((searchNameSoundexCode == "") || (searchAddressSoundexCode == "")) { MessageBox.Show("Word fragments are too small to generate a soundex code, please extend.", "Client inception"); return; } // calculate soundex threshold boundaries int searchNameLowerLimit = (int)searchNameSoundexCode.Substring(0, 1)[0] * 1000 + Convert.ToInt32(searchNameSoundexCode.Substring(1, 3)) - (int)SearchNameTolerance.Value; int searchNameUpperLimit = (int)searchNameSoundexCode.Substring(0, 1)[0] * 1000 + Convert.ToInt32(searchNameSoundexCode.Substring(1, 3)) + (int)SearchNameTolerance.Value; int searchAddressLowerLimit = (int)searchAddressSoundexCode.Substring(0, 1)[0] * 1000 + Convert.ToInt32(searchAddressSoundexCode.Substring(1, 3)) - (int)SearchAddressTolerance.Value; int searchAddressUpperLimit = (int)searchAddressSoundexCode.Substring(0, 1)[0] * 1000 + Convert.ToInt32(searchAddressSoundexCode.Substring(1, 3)) + (int)SearchAddressTolerance.Value; // X++ passthru query to get combined results string _dynamic = ""; _dynamic += @"str getDataSet() { Soundex objSoundexName, objSoundexAddress; DirPartyTable objDirPartyTable; DirPartyPostalAddressView obDirPartyPostalAddressView; CustTable objCustTable; int searchNameLowerLimit, searchNameUpperLimit; int searchAddressLowerLimit, searchAddressUpperLimit; str resultXml = '<?xml version=\'1.0\' encoding=\'UTF-8\' ?>'; searchNameLowerLimit = " + searchNameLowerLimit.ToString() + @"; searchNameUpperLimit = " + searchNameUpperLimit.ToString() + @"; searchAddressLowerLimit = " + searchAddressLowerLimit.ToString() + @"; searchAddressUpperLimit = " + searchAddressUpperLimit.ToString() + @"; resultXml += '<NewDataSet>'; while select Word, Position from objSoundexName where objSoundexName.SoundexCode >= searchNameLowerLimit && objSoundexName.SoundexCode <= searchNameUpperLimit && objSoundexName.ContextTableId == tableName2id('DirPartyTable') join Name from objDirPartyTable where objSoundexName.ContextRecId == objDirPartyTable.RecId join AccountNum from objCustTable order by AccountNum where objDirPartyTable.RecId == objCustTable.Party join PostalAddress, Address from obDirPartyPostalAddressView where objDirPartyTable.RecId == obDirPartyPostalAddressView.Party join Word, Position from objSoundexAddress where objSoundexAddress.SoundexCode >= searchAddressLowerLimit && objSoundexAddress.SoundexCode <= searchAddressUpperLimit && objSoundexAddress.ContextTableId == tableName2id('LogisticsPostalAddress') && objSoundexAddress.ContextRecId == obDirPartyPostalAddressView.PostalAddress { resultXml += '<SearchResults>'; resultXml += '<AccountNum>' + objCustTable.AccountNum + '</AccountNum>'; if (DirOrganization::find(objDirPartyTable.RecId).RecId) resultXml += '<Type>Organisation</Type>'; else resultXml += '<Type>Individual</Type>'; resultXml += '<Search_name>' + objSoundexName.Word + ' (' + num2str(objSoundexName.Position,0,0,1,0) + ')' + '</Search_name>'; resultXml += '<Name>' + objDirPartyTable.Name + '</Name>'; resultXml += '<Search_address>' + objSoundexAddress.Word + ' (' + num2str(objSoundexAddress.Position,0,0,1,0) + ')' + '</Search_address>'; resultXml += '<Address>' + obDirPartyPostalAddressView.Address + '</Address>'; resultXml += '</SearchResults>'; } resultXml += '</NewDataSet>'; return resultXml; }"; // load passthru return results into grid Mouse.OverrideCursor = Cursors.Wait; try { DataSet objDataSet = newDataSet(); objDataSet.ReadXml(newStringReader(PowerSQuirreL.runScript(_dynamic)), XmlReadMode.InferSchema); SearchGrid.ItemsSource = objDataSet.Tables[0].DefaultView; } catch (System.Exception ex) { SearchGrid.ItemsSource = null; } Mouse.OverrideCursor = null; } |
· The routine makes reference to a SoundexClass which is an assembly that needs to be included in this project. More details can be found here: [https://community.dynamics.com/ax/b/dynamicsax_wpfandnetinnovations/archive/2013/04/14/the-soundex-algorithm-en.aspx#.UYJ5k7XFX3Q]
· The code assumes “English” context. Ideally, the default language should be picked up from Dynamics context or an override drop-down (I’ve omitted this enhancement to simplify the code).
· Passthru X++ (with field substitution) is used to perform the querying (once the Soundex boundaries have been calculated). Ideally, this code needs to exist within an AOT class and be served via proxies or AIF. Again, this has been circumvented to “present” the solution in one listing.
· Dyamics query data is returned as an Xml string which is then converted to a .Net DataSet object. This way, it can be bound directly to the UI grid.
Define property |
publicstring AccountNum { get { return (string)GetValue(AccountNumProperty); } set { SetValue(AccountNumProperty, value); } } publicstaticDependencyProperty AccountNumProperty = DependencyProperty.Register( "AccountNum", typeof(string), typeof(Startup), newFrameworkPropertyMetadata(newPropertyChangedCallback(OnAccountNumChanged)), newValidateValueCallback(ValidateAccountNum)); privatestaticvoid OnAccountNumChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { Startup control = (Startup)d; control.RaiseEvent(newRoutedEventArgs(AccountNumChangedEvent)); } privatestaticbool ValidateAccountNum(object value) { returntrue; } publiceventRoutedEventHandler AccountNumChanged { add { AddHandler(AccountNumChangedEvent, value); } remove { RemoveHandler(AccountNumChangedEvent, value); } } publicstaticreadonlyRoutedEvent AccountNumChangedEvent = EventManager.RegisterRoutedEvent( "AccountNumChanged", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(Startup)); |
Add a double-click event handler to the UI grid |
public Startup() { InitializeComponent(); SearchGrid.MouseDoubleClick += newMouseButtonEventHandler(this.SearchGrid_OnDoubleClick); } |
Set the dependency property on doule-click |
privatevoid SearchGrid_OnDoubleClick(object sender, MouseButtonEventArgs e) { try { this.AccountNum = ((DataRowView)SearchGrid.SelectedItem)["AccountNum"].ToString(); } catch { } } |
ClassDeclaration |
publicclass FormRun extends ObjectRun { ClientInceptionUserControl.Startup _managedHost; } |
Init |
publicvoid init() { super(); _managedHost = ManagedHost.control(); _managedHost.add_AccountNumChanged(new ManagedEventHandler(this, 'ManagedHost_AccountNumChanged')); } |
ManagedHost_AccountNumChanged |
void ManagedHost_AccountNumChanged(System.Object sender, System.Windows.RoutedEventArgs e) { FormRun formRun; Args args = new Args(); ; args.name(formstr(CustTable)); args.record(CustTable::find(_managedHost.get_AccountNum())); formRun = ClassFactory.formRunClass(args); formRun.init(); formRun.run(); formRun.wait(); } |
C# |
privatevoid Create_Click(object sender, RoutedEventArgs e) { // X++ passthru query to create customer string _dynamic = ""; _dynamic += @"str writeCustomer() { // table objects DirParty objDirParty; DirPerson objDirPerson; DirPersonName objDirPersonName; DirOrganization objDirOrganization; DirOrganizationName objDirOrganizationName; DirPartyPostalAddressView objDirPartyPostalAddressView; CustTable objCustTable; // substitution values str firstName = '" + Forename.Text + @"'; str lastName = '" + Surname.Text + @"'; str organisationName = '" + OrganisationName.Text + @"'; str custGroup = '" + CustGroup.SelectedValue + @"'; str taxGroup = '" + TaxGroup.SelectedValue + @"'; str street = '" + Street.Text + @"'; str city = '" + City.SelectedValue + @"'; str county = '" + County.SelectedValue + @"'; str country = '" + Country.Text + @"'; str postCode = '" + Postcode.Text + @"'; // working variables str result = ''; DirPartyNumber partyNumber; int i; infolog.clear(); try { ttsBegin; // setup organisation if (organisationName != '') { objDirOrganization.initValue(); objDirOrganization.Name = organisationName; objDirOrganization.NameAlias = organisationName; objDirOrganization.LanguageId = 'en-gb'; if (objDirOrganization.validateWrite()) objDirOrganization.insert(); else { throw error('Failed [DirOrganization] validation.'); } objDirOrganizationName.initValue(); objDirOrganizationName.Organization = objDirOrganization.RecId; objDirOrganizationName.Name = organisationName; if (objDirOrganizationName.validateWrite()) { objDirOrganizationName.insert(); } else { throw error('Failed [DirOrganizationName] validation.'); } partyNumber = objDirOrganization.PartyNumber; } else { // setup individual objDirPerson.initValue(); objDirPerson.Name = firstName + ' ' + lastName; objDirPerson.NameAlias = firstName; objDirPerson.NameSequence = dirNameSequence::find('FirstLast').RecId; objDirPerson.LanguageId = 'en-gb'; if (objDirPerson.validateWrite()) objDirPerson.insert(); else { throw error('Failed [DirPerson] validation.'); } objDirPersonName.initValue(); objDirPersonName.Person = objDirPerson.RecId; objDirPersonName.FirstName = firstName; objDirPersonName.LastName = lastName; objDirPersonName.ValidFrom = DateTimeUtil::getSystemDateTime(); objDirPersonName.ValidTo = DateTimeUtil::maxValue(); if (objDirPersonName.validateWrite()) { objDirPersonName.insert(); } else { throw error('Failed [DirPersonName] validation.'); } partyNumber = objDirPerson.PartyNumber; } // setup customer objCustTable.initValue(); objCustTable.Party = DirPartyTable::find(partyNumber).RecId; objCustTable.AccountNum = NumberSeq::newGetNum(CustParameters::numRefCustAccount()).num(); objCustTable.CustGroup = custGroup; objCustTable.TaxGroup = taxGroup; if (objCustTable.validateWrite()) objCustTable.insert(); else { throw error('Failed [CustTable] validation.'); } // setup addresss objDirParty = DirParty::constructFromCommon(objCustTable); objDirPartyPostalAddressView.LocationName = 'PRIMARY'; objDirPartyPostalAddressView.Street = street; objDirPartyPostalAddressView.City = city; objDirPartyPostalAddressView.County = county; objDirPartyPostalAddressView.State = county; objDirPartyPostalAddressView.CountryRegionId = country; objDirPartyPostalAddressView.ZipCode = postCode; objDirParty.createOrUpdatePostalAddress(objDirPartyPostalAddressView); result = 'Customer created:' + objCustTable.AccountNum; ttsCommit; } catch { ttsAbort; for (i = 1; i <= infolog.line(); i++) { result += infolog.text(i) + '\n'; } } return result; }"; Mouse.OverrideCursor = Cursors.Wait; string str1 = PowerSQuirreL.runScript(_dynamic); MessageBox.Show(str1); if (str1.Substring(0, 16) == "Customer created") { this.AccountNum = str1.Replace("Customer created:", ""); } Mouse.OverrideCursor = null; } |