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

Dynamic references to NAV Page Web Services in C#

Note: There is an updated post about Dynamic references to NAV Page Web Services here.

When creating the very first (never published) version of Edit In Excel, it was loosely coupled, meaning that I did not have any Web references in the project to the Customer Page, Vendor Page or other pages. I read the WSDL and used XPath to traverse the XML and build up structures and I was able to attach to any Page Web Service.

The code wasn’t nice, and I was afraid that I would confuse more people than necessary if I posted that source. So I decided to go with a version, where I had a Web References in VS for every page you can connect to Excel.

The caveat of this approach is that everytime you customize the Customer Page, you need to recompile your Edit In Excel solution – AND you need to do some work if you want to add additional pages. Wouldn’t it be nice if we could avoid this?

If you study the code in the Edit In Excel, you will find that the type-strong web references really aren’t used that much, the majority of the code uses the type of the service, the type of the field enumeration etc.

Last week I stumbled over a very interesting blog called crowsprogramming. A special thanks to the author for this post:

C# – Dynamically Invoke Web Service At Runtime

which in details shows how to ask the read the WSDL, build a service description and compile it into an assembly, and it basically consists of 3 methods:

/// <summary>
/// Builds an assembly from a web service description.
/// The assembly can be used to execute the web service methods.
/// </summary>
/// <param name=”webServiceUri”>Location of WSDL.</param>
/// <returns>A web service assembly.</returns>
public static Assembly BuildAssemblyFromWSDL(Uri webServiceUri)

/// <summary>
/// Builds the web service description importer, which allows us to generate a proxy class based on the
/// content of the WSDL described by the XmlTextReader.
/// </summary>
/// <param name=”xmlreader”>The WSDL content, described by XML.</param>
/// <returns>A ServiceDescriptionImporter that can be used to create a proxy class.</returns>
private static ServiceDescriptionImporter BuildServiceDescriptionImporter(XmlTextReader xmlreader)

/// <summary>
/// Compiles an assembly from the proxy class provided by the ServiceDescriptionImporter.
/// </summary>
/// <param name=”descriptionImporter”></param>
/// <returns>An assembly that can be used to execute the web service methods.</returns>
private static Assembly CompileAssembly(ServiceDescriptionImporter descriptionImporter)

When you call the first method with our NAV WebServices URL, it reads the WSDL, creates a CodeDom of the proxy classes, compiles the proxy and returns an assembly, which you can reflect over – all it takes is the following line of code:

// create an assembly from the web service description
Assembly webServiceAssembly = BuildAssemblyFromWSDL(
new Uri(“http://localhost:7047/DynamicsNAV/WS/CRONUS_International_Ltd/Page/Customer”));

Type weak

So, what can you really do with an Assembly in your hand…

You cannot use statements like:

if (customer.No == “10000”)

if the webservice isn’t added to the project. How should Visual Studio know that there is a No field in customer. So any Web References you add dynamically will only be there to reflect over and use via reflection, but you can do everything using reflection – it is just harder.

First of all – we can enumerate the public types in the assembly:

// Create Service Reference
Type[] types = webServiceAssembly.GetExportedTypes();
foreach (Type type in types)
Console.WriteLine(type.ToString());

running this, will output the following:

Customer_Service
Customer
Blocked
Copy_Sell_to_Addr_to_Qte_From
Application_Method
Reserve
Shipping_Advice
Customer_Filter
Customer_Fields

Which are the public types from an assembly, which is the proxy to a NAV Customer Page Webservice. Knowing that all pages follows the same pattern and that everything in Edit In Excel uses reflection over these classes anyway, it really became too compelling to rip out the Web Service References and make everything dynamic (that post will follow this one).

Working with reflection

I am not going to go into detail about how reflection works and what you can do with reflection, but I will show some examples of how to work with the dynamic assembly. First of all we want to create our service class:

Type serviceType = webServiceAssembly.GetType(“Customer_Service”);
object service = Activator.CreateInstance(serviceType);

if using static web references, this would be Customer_Service service = new Customer_Service();

Now we need to set the UseDefaultCredentials property to true:

PropertyInfo useDefaultCredentials = service.GetType().GetProperty(“UseDefaultCredentials”);
useDefaultCredentials.SetValue(service, (object)true, new object[] { });

in other words, get the info-class about the property based on the type, and call the setValue on the propertyinfo, specifying the object instance you want to set the value in, the value and an empty array, specifying that there are no parameters for this call.

Using static web references, this would be service.UseDefaultCredentials = true;

Next thing we want to do, is to call ReadMultiple and get all customers:

MethodInfo readMultiple = service.GetType().GetMethod(“ReadMultiple”);
object[] customers = (object[])readMultiple.Invoke(service, new object[] { null, null, 0 });

You see the picture – get the method info-class based on the type, and invoke the instance based method, specifying the instance and an object[] which contains the parameters you want to use.

In the static world this would be Customer[] customers = service.ReadMultiple(null, null, 0);

Now, we have an array of objects and the objects are of type Customer – but we don’t know about the customer type – only from reflection, so if we want to write the names of all customers we have to do something like:

Type customerType = webServiceAssembly.GetType(“Customer”);
PropertyInfo no = customerType.GetProperty(“No”);
PropertyInfo name = customerType.GetProperty(“Name”);
foreach (object customer in customers)
Console.WriteLine(no.GetValue(customer, new object[] { }) + ” ” + name.GetValue(customer, new object[] { }));

Which in a static implementation would be foreach(Customer customer in customers) Console.Writeline(customer.No + “ “ + customer.Name);

When to use dynamic Web References?

So, by now you got it – and yes, it is WAY easier to work with static Web References, inserted in the solution and using the strongly typed classes and methods, so when would you use dynamic Web References?

My answer to this is: Whenever you want to make something generic, where you can connect to different pages and/or where you don’t mind that the pages gets customized. In the scenario, where you have a fixed contract for requesting order information from a web service in NAV, there is absolutely no reason to use dynamic web references. In cases where you are connecting to a page based web reference where you have control over the page, it is easier (and probably safer due to type checking) to use the type strong web service access and maybe using LINQ with NAV Web Services from my last post.

But… – for something like Edit In Excel, dynamic Web References is a gift – and is really useful. I also think that it could be very useful with Bugsy’s Sharepoint sample – I need to investigate that…

Privileges

I was concerned whether stuff like this would require elevated privileges, but it turns out, that as long as the DLL you are creating / calling is going to run in the same context as your application, this doesn’t require anything. I tested this out running as non-administrator with UAC (Windows Vista User Access Control) turned on.

As usual, you can download the DynamicWebReference solution here.

The next thing I will do, is to extract some of the code from the Edit In Excel and create a set of classes, which makes working with dynamic web references easier. This will of course then be rolled back into the Edit In Excel R2.

Enjoy

Freddy Kristiansen
PM Architect
Microsoft Dynamics NAV