Dotnet Tips for the Lazy Coder

Let’s face it, Dotnetting in Maxscript is one of the less inviting things to get into. It seems half your code is just namespace strings, you’re never sure if you should be using a DotNetClass, DotNetObject or DotNetControl, and the garbage collector is suddenly your worst enemy.

Below are a few small tips that should make your initial venture into Dotnet a bit less taxing on your keyboard. Some might seem obvious and trivial, but I think they are worth mentioning.

Note: This is in no way an introduction to DotNet, I highly recommend the articles by Paul Neale (PEN Productions) and Pete Addington (LoneRobot) for that purpose.

Omit the “System.Windows.Forms.” Prefix


If you’re starting out with Dotnet interfaces in Maxscript, you’ve probably typed these three words out more times than you’d care to acknowledge.
Well, good news then. You do not need to type them at all. The gracious developers at Autodesk have hardcoded this namespace (and sadly, only this namespace) to be presearched for a match when trying to resolve the type string.

What this means is that you can just omit the prefix for all types in the System.Windows.Forms namespace. Thats right:

DotNetObject "System.Windows.Forms.Button"

is exactly the same as

DotNetObject "Button"

Cache types inside loops


Even better than omitting part of the type string, is omitting all of it.
Whenever you are instantiating many DotNetObjects it is prudent to cache the type altogether.

Let’s look at a contrived example:

for i = 1 to 10000 collect dotNetObject "System.Drawing.Point" i i

--Execution time: 1015 ms
--Memory usage: 1280 KB

Now, how could we optimize this?
Well, there’s certainly no reason to allocate 10000 type strings. Let’s put the string into a variable outside of the loop:

ptStr = "System.Drawing.Point"
for i = 1 to 10000 collect dotNetObject ptStr i i

--Execution time: 990 ms
--Memory usage: 720 KB

This helped our memory usage a bit, but execution time is left almost unphased. This is because most of the time is spent looking up the correct object type to instantiate based on the type string.

The solution to this is to look up the type only once. This is done by creating a DotNetClass variable which hold the type information and inputting this variable in place of a type string into the DotNetObject constructor.
Now let’s see what happens when we precache the type:

ptCls = dotNetClass "System.Drawing.Point"
for i = 1 to 10000 collect dotNetObject ptCls i i

--Execution time: 93 ms
--Memory usage: 720 KB

A tenfold improvement! Not only are we saving MAXScript from allocating 10000 strings, we’re also saving it 10000 type checks.

Object, Class or Control?


This is an often confusing topic when starting out with Dotnet in MAXScript, though it’s really more simple than it sounds. I will not get into the differences between classes and objects here, you should really read the articles mentioned at the top of this post if you are not sure what the difference is. In a sentence, a DotNetClass represents a class, or a type, while a DotNetObject represents an instance of that type.

Use a DotNetClass whenever you want to access a static method or property of an object. e.g:

(dotNetClass "System.Environment").newLine --Access a static property
(dotNetClass "System.GC").Collect() --Execute a static method

Use a DotNetObject when you need to instantiate (construct) an object. If you need to add a control to a Dotnet form, you must create it using a DotNetObject. e.g.:

myButton = dotNetObject "Button"
myForm.Controls.Add myButton

The DotNetControl is a MAXScript wrapper around a Dotnet object which inherits from type System.Windows.Forms.Control. Use a DotNetControl only when you want to embed a Dotnet control inside a MAXScript rollout.
This wrapper enables you to use MAXScript style event handler syntax on the control. e.g.:

rollout myRollout "My Rollout"
(
    dotNetControl myBtn "Button"

    on myBtn click sender args do print sender
)

Enumerations


Enumeration values, for all syntax purposes, can be treated as static fields of a class. They can be accessed in one of two ways. Either using a type string:

myGfxObject.SmoothingMode = (dotNetClass "System.Drawing.Drawing2D.SmoothingMode").AntiAlias

Or simply using the property which we want to assign:

myGfxObject.SmoothingMode = myGfxObject.SmoothingMode.AntiAlias

This works because the property SmoothingMode is already of type System.Drawing.Drawing2D.SmoothingMode, and thus has a member AntiAlias.
Note that the fact that both the property we are assigning and the enumeration type have the same name is a coincidence and is not always the case.

Using the former syntax might be more readable in cases where the type of the enumeration is not immediately evident from the name of the property. Using the latter syntax is faster, more performant (only within the context of a large loop, it would never be noticable as a single call), and usually involves less typing.

Another way is to precache the enumeration type in your script:

smoothingMode = dotNetClass "System.Drawing.Drawing2D.SmoothingMode"
myGfxObject.SmoothingMode = smoothingMode.AntiAlias

This had the benefits of both methods.

Enumeration values can be combined using the dotNet.CombineEnums methods:

myButton.Anchor = dotNet.CombineEnums myButton.Anchor.Left myButton.Anchor.Right

This is equivalent to the c# bitwise or operator (|), and essentially means to use both values – in this case, anchor the control to both left side and right side of the parent control.
Note that not all enumerations can be combined in this way.

Use Factory Functions for Repetitive Controls


If you are initializing many controls of the same type, your code can get unnecessarily long. Consider the following:

btn1 = dotNetObject "Button"
btn1.Anchor = dotNet.combineEnums btn1.Anchor.Bottom btn1.Anchor.Right
btn1.Width = 50
btn1.Height = 25
btn1.Text = "Button 1"

btn2 = dotNetObject "Button"
btn2.Anchor = dotNet.combineEnums btn2.Anchor.Bottom btn2.Anchor.Right
btn2.Width = 80
btn2.Height = 25
btn2.Text = "Button 2"

...

btn10 = ...

This results in a massive ugly code block that is uncomfortable to revise. In place of this code we could create a function which returns a button, as such:

fn buttonFactory text width height =
(
    local btn = dotNetObject "Button"
    btn.Anchor = dotNet.combineEnums btn.Anchor.Left btn.Anchor.Right
    btn.Text = text
    btn.Width = width
    btn.Height = height
    btn
)

btn1 = buttonFactory "Button 1" 75 25
btn2 = buttonFactory "Button 2" 50 25

Of course, this function could be made as specific or as general as needed; Only add the properties which are not common to all controls as parameters. Similarly, it could even be turned into a general purpose control factory, accepting a type string, and as many parameters as needed.

fn controlFactory typeString params =
(
    --construct the control according to type string:
    local ctrl = dotNetObject typeString

    --initialize any common properties here:
    ctrl.Anchor = dotNet.combineEnums ctrl.Anchor.Bottom ctrl.Anchor.Right

    --initialize specified parameters:
    for p in params do setProperty ctrl p[1] p[2]

    --return the new control:
    ctrl
)

btn1 = controlFactory "Button" #(#(#width, 50), #(#height, 25), #(#text, "Button1"))
chk1 = controlFactory "CheckBox" #(#(#width, 80), #(#height, 20), #(#text, "CheckBox1"), #(#checked, true))