Open .net SDK command prompt here!

Barry Fitzgerald from Gumdrop books (a NAV 2009 customer) sent me a hint to another blog post, which in short talks about how to add a menu item to your folder menus like this:

image

http://www.theproblemsolver.nl/dotnet_faq_0016.htm

This would open a command prompt, starting in this directory ready to use Visual Studio tools like SN (which you need for Client extensibility) or RegAsm (which you need for COM development).

I found it pretty nice, so I thought I would share this.

On my machine, I have created the following registry key, which seems to work great.

image

Enjoy

Freddy Kristiansen
PM Architect
Microsoft Dynamics NAV

Sending data to NAV through a XMLPort

One of the things I showed at Directions US 2009 was how to send data to NAV via a XMLPort, and I promised to blog about that as well. In the meantime I found out, that Lars actually already wrote about this (and he also made a WebCast).

Please have a look at the NAV Team Blog for more info:

http://blogs.msdn.com/nav/archive/2009/11/06/using-dataports-with-web-services.aspx

The only reason for this post is, that people who attended my session at Directions might look for stuff like this on my blog – so, here you are.

Enjoy

Freddy Kristiansen
PM Architect
Microsoft Dynamics NAV

Edit In Excel R2 – Part 2 (out of 2) – the final pieces

It is time to collect the pieces.

The full Edit In Excel R2 solution looks like this

image

Slightly more complicated than the first version – but let me try to explain the pieces

NAVEditInExcel is the COM object, which we use from within NAV. This actually hasn’t changed a lot, the only small change is, that the EditInExcel method now takes a base URL, a company, a page and a view (compared to just a page and a view earlier).
NAVPageDynamicWebReference is the Dynamic Web Reference class and the NAVPageServiceHelper class – described here.
NAVPageFieldInfo contains the NAVFieldInfo class hierarchy for handling type weak pages, described here and used in the Conflict resolution dialog here.
NAVPageMergeForm is the conflict resolution dialog, described here.
NAVTemplate is the actual Excel Add-In which of course now makes use of Dynamic Page References and conflict resolution. It really haven’t changed a lot since the version described here – the major change is the pattern for handling conflict resolution.
EditInExcel Setup is the Client Setup program, this setup program needs to be run on all Clients
EditInExcelDemo is the Server Setup program, this setup program contains the Client Setup msi and places it in the ClientSetup folder for the ComponentHelper (which you can read about here) to autodeploy to clients. This setup also contains the .fob with the EditInExcel objects.

The Client Setup Program

Lets have a closer look at the Client Setup Program

image

This setup project includes primary output from the COM component and the Excel Add-in and calculated dependencies from that.

Note, that when deploying add-ins you have to add the .vsto and the .manifest files to the setup project yourself, the dependency finder doesn’t discover those. Also note, that all the vsto runtime dll’s etc are excluded from the install list, as we do not want to copy those DLL’s.

Instead I have built in a Launch condition for VSTO runtime 3.0, which is done in 2 steps:

image

First a Search on the Target Machine for component ID {AF68A0DE-C0CD-43E1-96DD-CBD9726079FD} (which is the component installation ID for VSTO 3.0 Runtime) and a launch condition stating that that search needs to return TRUE – else a message will appear with a URL for installing VSTO, which is:

http://www.microsoft.com/downloads/details.aspx?FamilyId=54EB3A5A-0E52-40F9-A2D1-EECD7A092DCB&displaylang=en

One more thing needed in the Client Setup program is to register the COM object. Now the Setup actually has a property you can set, indicating that the object should be registered as COM, but I couldn’t get that to work, so I added custom install actions to the NAVEditInExcel COM object:

image

and the code for the class, which is called by the installer looks like:

[RunInstaller(true)]
public partial class RegasmInstaller : Installer
{
public RegasmInstaller()
: base()
{
}

    public override void Commit(IDictionary savedState)
{
base.Commit(savedState);
Regasm(false);
}

    public override void Rollback(IDictionary savedState)
{
base.Rollback(savedState);
}

    public override void Uninstall(IDictionary savedState)
{
base.Rollback(savedState);
Regasm(true);
}

    private void Regasm(bool unregister)
{
string parameters = “/tlb /codebase”;
if (unregister)
parameters += ” /unregister”;
string regasmPath = RuntimeEnvironment.GetRuntimeDirectory() + @”regasm.exe”;
string dllPath = this.GetType().Assembly.Location;
if (!File.Exists(regasmPath))
throw new InstallException(“Registering assembly failed”);
if (!File.Exists(dllPath))
return;

        Process process = new Process();
process.StartInfo.CreateNoWindow = true;
process.StartInfo.UseShellExecute = false; // Hides console window
process.StartInfo.FileName = regasmPath;
process.StartInfo.Arguments = string.Format(“”{0}” {1}”, dllPath, parameters);
process.Start();

        // When uninstalling we need to wait for the regasm to finish,
// before continuing and deleting the file we are unregistering
if (unregister)
{
process.WaitForExit(10000);
try
{
System.IO.File.Delete(System.IO.Path.ChangeExtension(dllPath, “tlb”));
}
catch
{
}
}
}
}

All of the above is captured in the NAVEditInExcelR2.msi – which is the output from the Edit In Excel Setup project. Running this .msi on a client will check pre-requisites, install the right DLL’s, register the COM and you should be good to go.

The Server Setup Program

The Server Setup program actually just needs to place the Client Setup Program in a ClientSetup folder and the .fob (NAV Objects) in the ServerSetup folder.

There are no pre-requisites, no actions no nothing – just copy the files.

After Copying the files on the Server – you need to import the .fob, run the setup code unit and you should be good to go.

Note, that this requires ComponentHelper1.03 (which you can read about here and download here) to run.

Wrapping up…

So, what started out as being a small garage project, ended up being somewhat more complicated and way more powerful. It runs with Office 2007 and Office 2010 (even though you cannot modify the project when Office 2010 beta2 is installed) and even though you might not need the actual Edit In Excel functionality – there are pieces of this that can be used for other purposes.

The source for the entire thing can be downloaded here and the EditInExcel Demo msi can be downloaded here.

 

Happy holidays

 

Enjoy

Freddy Kristiansen
PM Architect
Microsoft Dynamics NAV

Auto Deployment of Client Side Components – take 2

Updated the link to the ComponentHelper msi on 12/11/2009

Please read my first post about auto deployment of Client side components here before reading this.

As you know, my first auto deployment project contained a couple of methods for automatically adding actions to pages, but as one of my colleagues in Germany (Carsten Scholling) told me, it would also need to be able to add fields to tables programmatically in order to be really useful.

In fact, he didn’t just tell me that it should do so, he actually send me a couple of methods to perform that.

The method signatures are:

AddToTable(TableNo : Integer;FieldNo : Integer;VersionList : Text[30];FieldName : Text[30];FieldType : Integer;FieldLength : Integer;Properties : Text[800]) : Boolean

and

AddTheField(TableNo : Integer;FieldNo : Integer;FieldName : Text[30];FieldType : Integer;FieldLength : Integer) SearchLine : Text[150]

And it can be used like:

// Add the fields
SearchLineLat := ComponentHelper.AddTheField(DATABASE::Customer, 66030, ‘Latitude’,  FieldRec.Type::Decimal, 0);
SearchLineLong := ComponentHelper.AddTheField(DATABASE::Customer, 66031, ‘Longitude’,  FieldRec.Type::Decimal, 0);

which just add’s the fields without captions or

// Add Latitude to Customer table
ComponentHelper.AddToTable(DATABASE::Customer, 66030, ‘VirtualEarthDemo1.01’, ‘Latitude’, FieldRec.Type::Decimal, 0,
‘CaptionML=[DAN=Breddegrad;DEU=Breitengrad;ENU=Latitude;ESP=Latitud;FRA=Latitude;ITA=Latitudine;NLD=Breedte];DecimalPlaces=6:8’);

Remember, that the table will be left uncompiled after doing this.

AddToTable actually calls AddTheField and after that it modifies the metadata to set the caption on the field:

AddToTable(TableNo : Integer;FieldNo : Integer;VersionList : Text[30];FieldName : Text[30];FieldType : Integer;FieldLength : Integer;Properties : Text[800]) : Boolean
changed := FALSE;
SearchLine := AddTheField(TableNo, FieldNo, FieldName, FieldType, FieldLength);
IF SearchLine <> ” THEN
BEGIN
GetTableMetadata(TableNo, Metadata);
IF AddToMetadataEx(TableNo, Object.Type::Table, Metadata, SearchLine, ”, ‘;’ + Properties, TRUE, FALSE) THEN BEGIN
SetTableMetadata(TableNo, Metadata, VersionList);
changed := TRUE;
END;
END;

AddTheField is the actual “magic”:

AddTheField(TableNo : Integer;FieldNo : Integer;FieldName : Text[30];FieldType : Integer;FieldLength : Integer) SearchLine : Text[150]
Field.SETRANGE(TableNo, TableNo);
Field.SETRANGE(“No.”, FieldNo);
SearchLine := ”;

IF Field.ISEMPTY THEN BEGIN
Field.TableNo := TableNo;
Field.”No.” := FieldNo;
Field.FieldName := FieldName;
Field.Type := FieldType;
Field.Class := Field.Class::Normal;
Field.Len := FieldLength;
Field.Enabled := TRUE;
Field.INSERT;

  Field.FINDFIRST;

  Len[1] := 4;
Len[2] := 20;
Len[3] := 15;

  IF STRLEN(FORMAT(FieldNo))   > Len[1] THEN Len[1] := STRLEN(FORMAT(FieldNo));
IF STRLEN(FieldName)         > Len[2] THEN Len[2] := STRLEN(FieldName);
IF STRLEN(Field.”Type Name”) > Len[3] THEN Len[3] := STRLEN(Field.”Type Name”);

  SearchLine := ‘    { ‘ + PADSTR(FORMAT(FieldNo), Len[1]) + ‘;  ;’ +
PADSTR(FieldName, Len[2]) + ‘;’ + PADSTR(Field.”Type Name”, Len[3]);
END;

The new ComponentHelper 1.03 msi can be downloaded here and my upcoming posts (e.g. Edit In Excel R2) will require this. If you only want to download the objects you can do so here (there is no changes in the NAVAddInHelper source (compared to the first post – that can be downloaded here).

Enjoy

Freddy Kristiansen
PM Architect
Microsoft Dynamics NAV

Conflict resolution when working with Web Services

It’s time to wrap up on Edit In Excel R2 – but before I do that, I will explain about another important feature in Edit In Excel R2 – conflict resolution.

In my last post on Edit In Excel R2 (found here) – I explained how to make Edit In Excel capable of taking data offline for editing. It doesn’t make much sense to do that unless you also have some sort of conflict resolution when you then try to save data.

Another important post, which you should read before continuing is this post, explaining about how to use dynamic web references and explains a little about some helper classes for enumerating fields and stuff.

This post is all about creating a conflict resolution dialog like this

image

which can be used in Edit In Excel – or in other scenarios, where you want the user to do conflict resolution.

Type weak

This dialog of course needs to be type weak – we are not about to write a conflict resolution dialog for each page we use as web services.

For this, I will use the NAVFieldInfo class hierarchy from this post and produce the following static method in a Windows Forms Form called NAVPageMergeForm:

/// <summary>
/// Resolve conflicts in a record
/// </summary>
/// <param name=”fields”>NAVFields collection</param>
/// <param name=”orgObj”>Original record</param>
/// <param name=”obj”>Record with your changes</param>
/// <param name=”theirObj”>Record with changes made by other users</param>
/// <param name=”mergedObj”>This parameter receives the merged object if the user presses accept</param>
/// <returns>DialogResult.Retry if the user pressed Accept Merged Values
/// DialogResult.Ignore if the user chose to ignore this record
/// DialogResult.Abort if the user chose to abort</returns>
public static DialogResult ResolveConflict(NAVFields fields, object orgObj, object myObj, object theirObj, out object mergedObj)
{
NAVPageMergeForm form = new NAVPageMergeForm();
// And set the values
form.SetValues(fields, orgObj, myObj, theirObj);
DialogResult response = form.ShowDialog();
if (response == DialogResult.Retry)
mergedObj = form.GetMergedObj();
else
mergedObj = null;
return response;
}

The method takes a Field Collection and three objects. The object as it looked before i started to make my changes (orgObj), the object with my changes (myObj) and the “new” object including the changes somebody else made in the database (theirObj). The last parameter is an out parameter meaning that this is where the method returns the merged object.

The return value of the dialog is a DialogResult, which can be Abort, Ignore og Retry.

Why Retry?

Why not OK?

Well, if you think of it, when you have merged the record you now have a new record which you have constructed – but while you were merging this record, somebody else might have changed the record again – meaning that we have to retry the entire thing and this might result in a new conflict resolution dialog popping up for the same record.

How to use the ResolveConflict method

As you can imagine, the usage of the method needs to follow a certain pattern and in my small sample app I have done this like:

bool retry;
do
{
retry = false;
try
{
service.Update(ref myChanges);
}
catch (Exception e)
{
string message = e.InnerException != null ? e.InnerException.Message : e.Message;
object theirChanges = null;
try
{
if (service.IsUpdated(customer2.Key))
theirChanges = service.Read(customer2.No);
}
catch { } // Ignore errors when re-reading
if (theirChanges != null)
{
object merged;
retry = (NAVPageMergeForm.ResolveConflict(fields, customer2, myChanges, theirChanges, out merged) == DialogResult.Retry);
if (retry)
myChanges = (Customer)merged;
}
else
retry = (MessageBox.Show(string.Format(“{0}nn{3}”, “Customer ” + customer2.No, message), “Unable to Modify Record”, MessageBoxButtons.RetryCancel, MessageBoxIcon.Error) == DialogResult.Retry);
}
} while (retry);

Compared to just saying service.Update(ref myChanges) – this is of course more complicated, but it adds huge value.

In Edit-In-Excel, this is of course captured i a method called UpdateRecord.

What happens in SetValues?

SetValues basically enumerates the field collection and adds values to a grid as you see in the image above, comparing the changes made by the various people and automatically merging values only changed by one user – displaying conflicts if the same field was modified by both.

/// <summary>
/// Set records in merge form
/// </summary>
/// <param name=”fields”>Field collection</param>
/// <param name=”orgObj”>Original record</param>
/// <param name=”obj”>Modified record</param>
/// <param name=”theirObj”>Changed record from WS</param>
internal void SetValues(NAVFields fields, object orgObj, object myObj, object theirObj)
{
this.mergedObj = theirObj;
this.fields = fields;

    foreach (NAVPageFieldInfo field in fields)
{
if (field.field != “Key”)
{
object orgValue = field.GetValue(orgObj);
if (orgValue == null) orgValue = “”;
object myValue = field.GetValue(myObj);
if (myValue == null) myValue = “”;
object theirValue = field.GetValue(theirObj);
if (theirValue == null) theirValue = “”;
object mergedValue;

            DataGridViewCellStyle myStyle = this.normalStyle;
DataGridViewCellStyle theirStyle = this.normalStyle;
DataGridViewCellStyle mergedStyle = this.normalStyle;

            bool iChanged = !orgValue.Equals(myValue);
bool theyChanged = !orgValue.Equals(theirValue);

            if (iChanged && theyChanged)
{
// Both parties changed this field
myStyle = modifiedStyle;
theirStyle = modifiedStyle;
if (myValue.Equals(theirValue))
{
mergedValue = myValue;
mergedStyle = this.modifiedStyle;
}
else
{
mergedValue = “”;
mergedStyle = this.conflictStyle;

                }
}
else if (theyChanged)
{
// “They” changed something – I didn’t
mergedValue = theirValue;
theirStyle = this.modifiedStyle;
mergedStyle = this.modifiedStyle;
}
else if (iChanged)
{
// I changed something – “they” didn’t
mergedValue = myValue;
myStyle = this.modifiedStyle;
mergedStyle = this.modifiedStyle;
}
else
{
// Nobody changed anything – merged value is ok
mergedValue = orgValue;
}
int rowno = this.mergeGridView.Rows.Add(field.field, orgValue, myValue, theirValue, mergedValue);
this.mergeGridView[2, rowno].ValueType = field.fieldType;
this.mergeGridView[3, rowno].ValueType = field.fieldType;
this.mergeGridView[4, rowno].ValueType = field.fieldType;

            this.mergeGridView[2, rowno].Style = myStyle;
this.mergeGridView[3, rowno].Style = theirStyle;
this.mergeGridView[4, rowno].Style = mergedStyle;
if (mergedStyle == this.conflictStyle)
{
if (this.mergeGridView.CurrentCell == null)
this.mergeGridView.CurrentCell = this.mergeGridView[0, rowno];
}
}
}
UpdateConflicts();
}

The rest is really manipulating button status depending on selection, setting values if you press My Changes, Their Changes or Original and in the end when the user pressed Accept changes we just return Retry and the caller will call and get the Merged Object, which basically just is

/// <summary>
/// Get Merged object
/// </summary>
/// <returns>the Merged record</returns>
internal object GetMergedObj()
{
int rowno = 0;
foreach (NAVPageFieldInfo field in fields)
{
if (field.field != “Key”)
{
field.SetValue(this.mergedObj, this.mergeGridView[4, rowno].Value, field.GetValue(this.mergedObj));
rowno++;
}
}
return this.mergedObj;
}

A small Test App

Here is a small test app, demonstrating how the conflict resolution can be provoked

Console.WriteLine(“Initialize Service…”);
Customer_Service service = new Customer_Service();
service.UseDefaultCredentials = true;

Console.WriteLine(“Read Customer 10000… – twice (two users)”);
// Read customer twice
Customer customer1 = service.Read(“10000”);
Customer customer2 = service.Read(“10000”);

Console.WriteLine(“One user changes Customer 10000…”);
// Change customer 1
customer1.Phone_No = “111-222-3333”;
customer1.Address_2 = “an address”;
service.Update(ref customer1);

Console.WriteLine(“Other user tries to change Customer 10000”);
NAVFields fields  = new NAVFields(typeof(Customer), typeof(Customer_Fields));
Customer myChanges = (Customer)GetCopy(customer2);

myChanges.Phone_No = “222-333-4444”;
myChanges.Name = “The Cannon Group, Inc.”;

Write the data according to the pattern shown above – using conflict resolution and all (and after that – clean up the data, so that we can run the test app again)

Console.WriteLine(“Reset Data…”);

// Clear any updated data
Customer customer = service.Read(“10000”);
customer.Phone_No = “”;
customer.Address_2 = “”;
customer.Name = “The Cannon Group PLC”;
service.Update(ref customer);

Console.WriteLine(“nTHE END”);
Console.ReadLine();

BTW – the GetCopy method looks like this:

private static object GetCopy(object obj)
{
Type typ = obj.GetType();
MethodInfo mi = typ.GetMethod(“MemberwiseClone”, BindingFlags.Instance | BindingFlags.NonPublic);
return mi.Invoke(obj, null);
}

The ConflictResolution solution can be downloaded here.

Enjoy

Freddy Kristiansen
PM Architect
Microsoft Dynamics NAV

Dynamic references to NAV Page Web Services in C# – take 2

In this post from April, I explained how to make dynamic references to page based Web Services, but the post really left the developer with a lot of manual work to do using reflection.

So – I thought – why not create a couple of helper classes which makes it easier.

Basically I have created a generic NAVPageServiceHelper class, which encapsulates all the heavy lifting of reflection and leaves the developer with a set of higher level classes he can use.

The service helper will have a collection of classes explaining various information about the fields and has methods for getting or setting the value (and setting the corresponding _Specified automatically as well).

The primary reason for making this is of course to make Edit In Excel bind to any page without changing anything, but the method can be used in a lot of other scenarios.

2 projects: NAVPageFieldInfo and NAVPageDynamicWebReference

I split the PageServiceHelper and the PageFieldInfo into two seperate projects. NAVPageFieldInfo just contains the FieldInfo classes for all the supported field types and a collection class.

NAVPageFieldInfo is the abstract base class
BooleanFieldInfo is the field info class for a boolean field
OptionFieldInfo is the field info class for an option field
IntFieldInfo is the field info for…

You get it – all in all, the following types are supported:

String, Decimal, DateTime, Int, Option, Boolean

Furthermore, there is a class called NAVFields, which derives from List<NAVFieldInfo>, for keeping a collection of the fields.

NAVFields has a method called PopulateFieldsCollection, which takes an object type and a fields enum type and based on this, instantiates all the NAVFieldInfo classes – let’s look at the code.

/// <summary>
/// Populate Fields Collection with NAVPageFieldInfo for all properties in the record
/// Should works with any NAV 2009 Page exposed as WebService
/// </summary>
/// <param name=”objType”>Type of Object (typeof(Customer), typeof(Vendor), …)</param>
/// <param name=”fieldsType”>Type of the Enum holding the property names</param>
private void PopulateFieldsCollection(Type objType, Type fieldsType)
{
// Key property is not part of the Enum
// Add it manually as the first field
AddField(“Key”, objType);

    // Run through the enum and add all fields
foreach (string field in Enum.GetNames(fieldsType))
{
AddField(field, objType);
}
}

/// <summary>
/// Add a NAVPageFieldInfo for a field to the fields collection
/// </summary>
/// <param name=”field”>Field name</param>
/// <param name=”objType”>Type of Object in which the field is (typeof(Customer), typeof(Vendor), …)</param>
private void AddField(string field, Type objType)
{
field = VSName(field);
PropertyInfo pi = objType.GetProperty(field, System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
if (pi != null)
{
NAVPageFieldInfo nfi = NAVPageFieldInfo.CreateNAVFieldInfo(objType, field, pi, objType.Namespace);
if (nfi != null)
{
// If we encounter unknown Field Types, they are just ignored
this.Add(nfi);
}
}
}

As you can see, the AddField method calls a static method on NAVPageFieldInfo to get a FieldInfo class of the right type created. That method looks like:

/// <summary>
/// Create a NAVPageFieldInfo object for a specific field
/// </summary>
/// <param name=”field”>Name of the property</param>
/// <param name=”pi”>PropertyInfo for the property on the record object</param>
/// <param name=”ns”>Namespace for the record object (namespace for the added WebServices proxy class)</param>
/// <returns>NAVPageFieldInfo or null if the type isn’t supported</returns>
public static NAVPageFieldInfo CreateNAVFieldInfo(Type objType, string field, System.Reflection.PropertyInfo pi, string ns)
{
if (pi.PropertyType == typeof(string))
{
// String Property – is it the KeyField
if (field == “Key”)
return new KeyFieldInfo(field, pi, null);
else
return new StringFieldInfo(field, pi, null);
}
PropertyInfo piSpecified = objType.GetProperty(field + “Specified”, System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
if (pi.PropertyType == typeof(decimal))
{
// Decimal Property
return new DecimalFieldInfo(field, pi, piSpecified);
}
if (pi.PropertyType == typeof(int))
{
// Integer Property
return new IntFieldInfo(field, pi, piSpecified);
}
if (pi.PropertyType == typeof(bool))
{
// Boolean Property
return new BooleanFieldInfo(field, pi, piSpecified);
}
if (pi.PropertyType == typeof(DateTime))
{
// DateTime Property
return new DateTimeFieldInfo(field, pi, piSpecified);
}
if (pi.PropertyType.Namespace == ns)
{
// Other Property Types, in the same namespace as the object
// These are enum’s – set up restrictions on OptionFields
return new OptionFieldInfo(field, pi, piSpecified, Enum.GetNames(pi.PropertyType));
}
return null;
}

No more magic!

And actually the constructor for NAVFields – takes the object type and the field type as parameters for the constructor:

public NAVFields(Type objType, Type fieldsType)
: base()
{
this.PopulateFieldsCollection(objType, fieldsType);
}

Meaning that all it takes to utilize the NAVFieldInfo subsystem is instantiating the NAVFields class, which doesn’t necessarily need a dynamic web reference helper, but could also be instantiated through:

NAVFields fields = new NAVFields(typeof(Customer), typeof(Customer_Fields));

If you have some code, which needs to access data loosely coupled, NAVFields is a great way to get going.

The other project is the NAVDynamicPageWebReference – which really is a combination of the Dynamic Web References post from April and a Page Service Helper class.

The way you get a reference to the Dynamic Web Reference is much like in the post from April:

Assembly customerPageRef = NAVPageDynamicWebReference.BuildAssemblyFromWSDL(
new Uri(“
http://localhost:7047/DynamicsNAV/WS/CRONUS%20International%20Ltd./Page/Customer”), 5000);

Based on this, you now instantiate the Service Helper with the Assembly and the name of the Page:

NAVPageServiceHelper serviceHelper = new NAVPageServiceHelper(customerPageRef, “Customer”);

Using the Page Service Helper

The Page Service Helper then uses NAVFields so that you can do stuff like:

foreach (NAVPageFieldInfo fi in serviceHelper.Fields)
Console.WriteLine(fi.field + ” ” + fi.fieldType.Name);

The properties currently in the Service Helper are:

Fields is a NAVFields (a list of NAVFieldInfo derived classes)
PrimaryKeyFields is an array NAVFieldInfo classes (from Fields) which makes out the primary key of the record
GetFieldsType returns the type of the Field enumeration
GetObjectType returns the type of the records handles through this Service
ReadMultiple reads the records matching an array of filters (calls the ReadMultiple on the Service)
CreateFilter creates a filter spec based on a field and a criteria
Read reads a record based on a primary key (creates a filter spec for the primary key and calls ReadMultiple)
Update updates a record (calls the Update method on the Service)
Create creates a record (calls the Create method on the Service)
Delete deletes a record matching a key
ReRead reads an updated instance of a record (calls the Read method on the Service with the key fields)
IsUpdated checks whether the record is updated (calls the IsUpdated method on the Service)
GetFiltersFromView creates an array of filter specs based on a view (from GETVIEW in AL Code)

An example of how to read customer 10000 and print the name would be:

object cust = serviceHelper.Read(“10000”);
Console.WriteLine(serviceHelper.Fields[“Name”].GetValue(cust));

Note, that you find the Field – and on the field, you call GetValue and specify the record instance.

If you need to Display the name of all customers with location code yellow you would write

ArrayList filters = new ArrayList();
filters.Add(serviceHelper.CreateFilter(“Location_Code”, “Yellow”));
object[] customers = serviceHelper.ReadMultiple(filters);
foreach (object customer in customers)
Console.WriteLine(serviceHelper.Fields[“Name”].GetValue(customer));

Or you could create a Customer by writing

object newcust = System.Activator.CreateInstance(serviceHelper.GetObjectType());
serviceHelper.Fields[“Name”].SetValue(newcust, “Freddy Kristiansen”, DBNull.Value);
newcust = serviceHelper.Create(newcust);
Console.WriteLine(serviceHelper.Fields[“No”].GetValue(newcust));

As mentioned before, the Page Service Helper was primarily created for making Edit In Excel and other projects, where you are using loosely coupled Page Web Service Access.

For a lot of other usages, this is overkill and you should rather use Web References in Visual Studio and have a strongly typed contract with the Web Service.

You can download the projects and the small test program here.

Enjoy

Freddy Kristiansen
PM Architect
Microsoft Dynamics NAV

Displaying Company information in Card pages

I received a question from a customer, who is running multiple companies and often have multiple instances of NAV open with different companies. On the main page, they do have information about what company is the active:

image

but when they have opened a number of Task Pages in the various instances of NAV, they cannot distinguish one from the other.

Example:

image

This image tells you the page and the Customer name – and you can easily identify the right page when Alt+TAB’ing between pages, but if you have multiple companies this doesn’t help you a lot.

So what determines the caption?

The fields used in the caption on a page is determined by:

DataCaptionExpr on the page. This is an expression, which can use fields, functions etc. to build up a caption. If that isn’t defined, the client looks for

DataCaptionFields on the page. This is a collection of fields, which are used to build the caption by adding them together with a character 183 (middle dot) between them. If that isn’t defined, the client looks for

DataCaptionFields on the table, which basically is the same as DataCaptionFields on the page.

In a standard NAV, there is no DataCaptionExpr nor DataCaptionFields defined on the Customer Card, but on the Customer table you find:

image

In order to add the Company name behind the caption you will need to change the DataCaptionExpr on the Customer Card to f.ex.

“No.” + ‘ · ‘ + Name + ‘ [‘+COMPANYNAME+’]’

which would cause the Customer Card to look like

image

You can of course select to change the expression to whatever you like – or maybe create some function, which automagically returns a caption, only real flipside is that you need to modify the card pages, on which you need this functionality. In the end this is probably not a very large number.

Enjoy

Freddy Kristiansen
PM Architect
Microsoft Dynamics NAV

What COMPANY to use?

As you know, when creating an application consuming NAV Web Services you need to specify the Company name as part of the URL to the Web Service, but what company should you be using?

Some applications are web front-ends placing data from the web application into NAV. For applications like this you typically would have a config file in which you specify what company things needs to go to. For these applications, this post adds no further value.

Other applications are integration applications, like a lot of the applications you can see on my blog:

  • Search
  • “My” gadgets
  • MAP
  • Edit In Excel

for all of these applications, it really doesn’t make sense to run with a different company than the users default company.

Example – if you search through your NAV data – you really want to search through the data in the active company – not just any company.

Wouldn’t it be nice if you could type in the URL

/Codeunit/Search”>http://localhost:7047/DynamicsNAV/WS/<default>/Codeunit/Search

and then the <default> would be replaced by the authenticated users default company – unfortunately this doesn’t work (I have suggested this feature for v7 though:-)). Instead, we have to do the work in the Web Service consuming application. Easiest solution is of course to create a Codeunit with a function, returning the default company of a user, call that and then build your URL for calling the Page / Codeunit web service.

A function like that could be:

GetDefaultCOMPANY() : Text[30]
Session.SETRANGE(“My Session”,TRUE);
Session.FINDFIRST;
WindowsLogin.SETRANGE(ID,Session.”User ID”);
WindowsLogin.FINDFIRST;
UserPers.SETRANGE(“User SID”,WindowsLogin.SID);
UserPers.FINDFIRST;
EXIT(UserPers.Company);

The problem with this approach is (as you probably already figured out) that every call to a Web Service will require 2 roundtrips instead of one and for Page based Web Service access there really isn’t much you can do better.

For Codeunit based Web Service access you can however avoid a lot of these roundtrips by using a very simple pattern in the way you write your functions. I have rewritten my search method to return a Text[30] and start off with the following lines of code:

company := GetDefaultCOMPANY();
IF company <> COMPANYNAME THEN
EXIT(company);

and the consumer will have to build up the URL for the Web Service in code with whatever company (the first in the list of companies would be just fine), call the web service and if it returns a different company than the one used to invoke the web service, build a new URL and try again.

In the Search gadget this would look like (the lines in Red are the important changes)

// the “real” search function
function doSearch(searchstring) {
    specifiedCompany = GetCompany();
usedCompany = specifiedCompany;
if (specifiedCompany == “default”) {
if (myCompany == “”) {
Companies = GetCompanies();
if (Companies != null)
myCompany = Companies[0].text;
}
usedCompany = myCompany;
}

    // Get the URL for the NAV 2009 Search Codeunit
var URL = GetBaseURL() + encodeURIComponent(usedCompany) + “/Codeunit/Search”;

    // Create XMLHTTP and send SOAP document
xmlhttp = new ActiveXObject(“Msxml2.XMLHTTP.6.0”);
xmlhttp.open(“POST”, URL, false, null, null);
xmlhttp.setRequestHeader(“Content-Type”, “text/xml; charset=utf-8”);
xmlhttp.setRequestHeader(“SOAPAction”, “DoSearch”);
xmlhttp.Send(‘<?xml version=”1.0″ encoding=”utf-8″?><soap:Envelope xmlns:soap=”‘ + SoapEnvelopeNS + ‘”><soap:Body><DoSearch xmlns=”‘ + CodeunitSearchNS + ‘”><searchstring>’ + searchstring + ‘</searchstring><result></result></DoSearch></soap:Body></soap:Envelope>’);

    // Find the result in the soap result and return the rsult
xmldoc = xmlhttp.ResponseXML;
xmldoc.setProperty(‘SelectionLanguage’, ‘XPath’);
xmldoc.setProperty(‘SelectionNamespaces’, ‘xmlns:soap=”‘ + SoapEnvelopeNS + ‘” xmlns:tns=”‘ + CodeunitSearchNS + ‘”‘);

    userCompany = xmldoc.selectSingleNode(‘//tns:return_value’);
myCompany = userCompany.text;

    if ((specifiedCompany == “default”) && (myCompany != usedCompany)) {
// Default company has changed – research
return doSearch(searchstring);
}

   … do the actual searching

}

In this sample I use three variables:

specifiedCompany is the company specified in the config file (default means use users default company)

usedCompany is the company used to invoke the last WS method

myCompany is my current belief of the users current company, which gets replaced if a method returns a new default company.

Using a pattern like this will help lowering the number of round trips and still allow your consuming application to use the users default company.

This “trick” is only possible in NAV 2009 SP1. NAV 2009 RTM will change the users default company to the company you use to invoke the Web Service with – which again will cause the above function to always return the same company name as the one you invoke the Web Service with.

Enjoy

Freddy Kristiansen
PM Architect
Microsoft Dynamics NAV

Extending page Web Services (and creating a Sales Order again)

It has been working in the same way since NAV 2009, but I still get asked often how this works, so why not write up a quick post on this. I also realize that my prior post on how to create Sales Orders through Web Services was made very complex due to compatibility with NAV 2009.

This post only works in NAV 2009 SP1 and will show how to extend the Order page with a Post function and show how to Create a Sales Order from C# and post it.

Extending the page

First of all, we need to create a codeunit with the function, we want to add to the Order page.

image

Then we expose this codeunit with the same name as the page we want to extend, without putting a check in the published column

image

Note: All functions in the codeunit needs to have the first parameter be of the same type as the base record as the page you want to extend, else the page will no longer be available and you will get an error in the event log on the Service Tier.

Now taking a look at the WSDL in a browser will show us the new function as a first class citizen

image

and we can start using this.

Creating a Sales Order through Web Services

This might seem like repeating myself from a prior post, but that post did contain a lot of other information, which really isn’t necessary if you only target SP1.

Creating an order is a 3 step process:

  1. Create the Order Header
  2. Fill out the Order Header and create the Order lines
  3. Fill out the Order lines

Creating the Order header

Is really simple

Order_Service service = new Order_Service();
service.UseDefaultCredentials = true;

Order order = new Order();
service.Create(ref order);

After this we have a Order Number and an empty order – exactly like leaving the order No. field on the Sales Order Page.

Fill out the Order Header and create the Order lines

In this sample I will just fill out the Sell_to_Customer_No – a number of the other Order Header fields will be auto-updated when updating the order

order.Sell_to_Customer_No = “10000”;

Then we need to create the Order lines – in this sample I will create 5. BTW – It is NOT trivial to add an order line after the fact, so I suggest you add the needed number of lines in one go:

order.SalesLines = new Sales_Order_Line[5];
for (int i = 0; i < 5; i++)
order.SalesLines[i] = new Sales_Order_Line();
service.Update(ref order);

Fill out the Order lines

In this sample, I will just create 5 lines with green ROME guest chairs.

for (int i = 0; i < 5; i++)
{
order.SalesLines[i].Type = OrderPageRef.Type.Item;
order.SalesLines[i].No = “1960-S”;
order.SalesLines[i].Quantity = 1;
}
service.Update(ref order);

That’s it – the order is created and you can find it in the Client.

And at last… – Post the order

Having created the order, now it is time to post the order

service.PostOrder(order.Key);

As you can see, the function takes a Record parameter, but we give it a Key.

Note, that calling a function does not make an implicit Update – meaning that if you have done changes to the record in C# and call the function, you will get an error when calling update later. Reason – the PostOrder function has changed the record and will tell you that the record was changed by another user.

After calling a function on a page you will need to Re-read the record if you need to do more work.

That’s it

Enjoy

Freddy Kristiansen
PM Architect
Microsoft Dynamics NAV

Synchronize A/D users to NAV

During my work with demos like Edit In Excel, I wanted to make sure that these things would work in all localized versions of NAV 2009 SP1 – meaning that I needed to install 14 different databases and 14 running Service Tier’s. Having done that, I also wanted to allow my colleagues who needed to check something, access to these service tiers.

For a geek (like me:-)), that problem looks like something you need to write an application for, even though it probably takes more time than it would be to add the users one by one whenever needed, but it certainly is more fun to write the application – and… maybe somebody else can actually use the ideas from this to do something cool.

How to do?

Basically what I want to do is, to enumerate the Remote Desktop Users of the computer and make sure that these users are SUPER in NAV. Now, I can do this in NAV calling out to a COM object enumerating my users – but that really wouldn’t help me, because I would have to start NAV with every service tier or database and launch that action.

So, first I created a codeunit in NAV, exposed this codeunit as Web Services. Then I created a console application running on a schedule on my server, which enumerates the users and invoke the Web Service function to check all users are created in NAV.

Of course the scheduled application has to run with elevated permissions and that user needs to be able to change users in NAV as well.

The NAV code

The codeunit contains two functions:

ImportUserSID(SID : Text[119];Role : Code[20]) : Boolean
IF NOT Winlogin.GET(SID) THEN
EXIT(FALSE);
Winlogin.INIT;
Winlogin.SID := SID;
Winlogin.INSERT();
IF NOT WinAccess.GET(SID) THEN
BEGIN
WinAccess.INIT;
WinAccess.”Login SID” := SID;
WinAccess.”Role ID” := Role;
WinAccess.INSERT;
END;
EXIT(TRUE);

and

SynchronizeUsers()
DATABASE.SYNCHRONIZEALLLOGINS();

BTW. Calling SynchronizeAllLogins running through Web Services didn’t seem to work when called through Web Services and running enhanced database security – whether this is a bug or a problem in my setup – I don’t know, but I am going to file a bug on it.

As you can see, the ImportUserSID does not modify the role if the user is already in the login table – it could of course be modified to do that easily, but it wasn’t necessary for my usage.

The C# code for enumerating an A/D group

Lets just look at the code:

DirectoryEntry dir = new DirectoryEntry(“WinNT://localhost/Administrators”);
Console.WriteLine(“Enumerating users”);
foreach (object obj in (IEnumerable)dir.Invoke(“members”))
{
DirectoryEntry user = new DirectoryEntry(obj);
Console.WriteLine(user.Path);
}
Console.WriteLine(“Done”);
Console.ReadLine();

Running this on my computer outputs the following:

image

The next step is, to get the SID’s for each user – now you would think that there should be an SID function on the user object returning a string, but unfortunately it isn’t that simple. Luckily somebody invented the Internet – and luckily somebody was kind enough to post some information for us to use.

Have a look at http://www.netomatix.com/GetUserSid.aspx where I found the following code, which seems to work for the purpose.

On the user object you have a property collection. One of these properties is called objectSid, which is a byte[] and that can be transformed into a SID string using the following function:

private static string ConvertByteToStringSid(Byte[] sidBytes)
{
StringBuilder strSid = new StringBuilder();
strSid.Append(“S-“);
try
{
// Add SID revision.
strSid.Append(sidBytes[0].ToString());
// Next six bytes are SID authority value.
if (sidBytes[6] != 0 || sidBytes[5] != 0)
{
string strAuth = String.Format
(“0x{0:2x}{1:2x}{2:2x}{3:2x}{4:2x}{5:2x}”,
(Int16)sidBytes[1],
(Int16)sidBytes[2],
(Int16)sidBytes[3],
(Int16)sidBytes[4],
(Int16)sidBytes[5],
(Int16)sidBytes[6]);
strSid.Append(“-“);
strSid.Append(strAuth);
}
else
{
Int64 iVal = (Int32)(sidBytes[1]) +
(Int32)(sidBytes[2] << 8) +
(Int32)(sidBytes[3] << 16) +
(Int32)(sidBytes[4] << 24);
strSid.Append(“-“);
strSid.Append(iVal.ToString());
}

        // Get sub authority count…
int iSubCount = Convert.ToInt32(sidBytes[7]);
int idxAuth = 0;
for (int i = 0; i < iSubCount; i++)
{
idxAuth = 8 + i * 4;
UInt32 iSubAuth = BitConverter.ToUInt32(sidBytes, idxAuth);
strSid.Append(“-“);
strSid.Append(iSubAuth.ToString());
}
}
catch (Exception ex)
{
return “”;
}
return strSid.ToString();
}

Using this function, you can now write out the SID’s for each user:

DirectoryEntry dir = new DirectoryEntry(“WinNT://localhost/Administrators”);
Console.WriteLine(“Enumerating users”);
foreach (object obj in (IEnumerable)dir.Invoke(“members”))
{
DirectoryEntry user = new DirectoryEntry(obj);
Console.WriteLine(user.Path);
System.DirectoryServices.PropertyCollection col = user.Properties;
byte[] sidBytes = col[“objectSid”].Value as byte[];
if (sidBytes != null)
{
string strSid = ConvertByteToStringSid(sidBytes);
if (!string.IsNullOrEmpty(strSid))
{
Console.WriteLine(“SID=” + strSid);
}
}
}
Console.WriteLine(“Done”);
Console.ReadLine();

Of course we are not in the business of writing SID’s for users in a console application, but you should now have the building blocks for creating whatever mechanism to add A/D users to NAV, exposing the codeunit we talked about at first and then calling these web services functions from the console application.

Good luck

Enjoy

Freddy Kristiansen
PM Architect
Microsoft Dynamics NAV