Using WinForms in Journals

We've looked at various graphical interface elements to interact with the user:

However, sometimes you need something customized to the task at hand; it's time to create our own input form.


Prerequisites

This tutorial assumes you are fairly comfortable working with forms and controls and now want to know how to use them with your journals. If you don't know a radio button from a combo box, you will want to check out one of the VB books from your local library (check out some suggestions on the resource page) and spend some time noodling around in an integrated development environment (IDE). Speaking of which, you will need an IDE to create forms for user interaction; well, technically that's untrue, our resulting journal file will be plain text - you only really need notepad, a lot of know-how, and the patience of Job to create userforms. If, like me, you only have one of those - and don't like using notepad, get an IDE. I'm using (and recommend) VB.net express, but there are other options out there (see the resource page for some links). It will also be helpful to know some object oriented programming (OOP) concepts such as classes, properties, methods, etc., but we'll explore the necessary bits as we go.



Goal

The journal that we'll build will check the work part for the existence of a particular attribute. It will then display a form showing the value of that attribute (if any) in a textbox; if the user enters a new value and presses OK, the part attribute will be updated. Nothing too exciting in and of itself, but it will demonstrate how to display a user form that you have created and pass information between the form object and the journal module.



The Project

After installing VB.net express and configuring the NX code wizards, I started a new NXOpen project. The NX code wizard asked a few questions after which it presented me some starting code based on my answers. I next added a form object to my new project and added: a label, a textbox, and two buttons. The buttons I named btnOk and btnCancel to help keep them straight (Button1 and Button2 were not descriptive enough for my taste). I didn't spend too much time on the layout, as evidenced below:




journal form




When the journal starts, the Main subroutine will be the first to execute (as usual). What we want to do is:

  • check for the existence of a particular attribute
  • display a form showing the attribute (if it exists) and allow the user to type in a new value
  • and finally, assign the entered value to the part attribute

Reading the value of an attribute is fairly easy; the questions that remain are: how do we show the form that we've created, how do we pass information from the Main subroutine to the form, and how do we pass information from the form back to the journal?



The Code

To pass information to the journal, we could use some module level public variables. While this approach would work and would be convenient in a project of this size; we'll use properties to manage this information. Using properties rather than public variables gives us two advantages: we can more easily do data validation and it avoids the use of global variables. We can write code that ensures the value passed into the property is usable and this code will run every time the property value is changed. It is a programming 'best practice' to avoid the use of global variables where possible. By their nature, global variables can be changed by any line of code in the project. As your code projects grow larger and more complex, it will take considerably more effort on your part to track down bugs in the code. By limiting the scope of your variables and by limiting access to those variables, you can greatly reduce the time spent debugging.



The Module Code

Option Strict Off
Imports System
Imports NXOpen
 
Module Module1
 
    Private Const _attributeTitle As String = "PROJECT"
    Public ReadOnly Property AttributeTitle() As String
        Get
            Return _attributeTitle
        End Get
    End Property
 
    Private _attributeValue As String = ""
    Public Property AttributeValue() As String
        Get
            Return _attributeValue
        End Get
        Set(ByVal value As String)
            _attributeValue = value
        End Set
    End Property
 
 
    Sub Main()
 
        Dim theSession As Session = Session.GetSession()
        If IsNothing(theSession.Parts.Work) Then
            'active part required
            Return
        End If
 
        Dim workPart As Part = theSession.Parts.Work
        Dim lw As ListingWindow = theSession.ListingWindow
        lw.Open()
 
        Const undoMarkName As String = "NXJ form demo journal"
        Dim markId1 As Session.UndoMarkId
        markId1 = theSession.SetUndoMark(Session.MarkVisibility.Visible, undoMarkName)
 
        Dim myAttributeInfo As NXObject.AttributeInformation
 
        Try
            myAttributeInfo = workPart.GetUserAttribute(_attributeTitle, NXObject.AttributeType.String, -1)
            _attributeValue = myAttributeInfo.StringValue
 
        Catch ex As NXException
            If ex.ErrorCode = 512008 Then
                'attribute not found
            Else
                'unexpected error: show error message, undo, and exit journal
                MsgBox(ex.ErrorCode & ": " & ex.Message)
                theSession.UndoToMark(markId1, undoMarkName)
                Return
            End If
 
        Finally
 
        End Try
 
        'create new form object
        Dim myForm As New Form1
        'set form object properties (current part attribute title and value)
        myForm.AttributeTitle = _attributeTitle
        myForm.AttributeValue = _attributeValue
        'display our form
        myForm.ShowDialog()
 
        If myForm.Canceled Then
            'user pressed cancel, exit journal
            Return
        Else
            'user pressed OK, assign value from form to part attribute
            _attributeValue = myForm.AttributeValue
            workPart.SetUserAttribute(_attributeTitle, -1, _attributeValue, Update.Option.Later)
 
        End If
 
        lw.Close()
 
    End Sub
 
 
    Public Function GetUnloadOption(ByVal dummy As String) As Integer
 
        'Unloads the image when the NX session terminates
        'GetUnloadOption = NXOpen.Session.LibraryUnloadOption.AtTermination
 
        '----Other unload options-------
        'Unloads the image immediately after execution within NX
        GetUnloadOption = NXOpen.Session.LibraryUnloadOption.Immediately
 
        'Unloads the image explicitly, via an unload dialog
        'GetUnloadOption = NXOpen.Session.LibraryUnloadOption.Explicitly
        '-------------------------------
 
    End Function
 
End Module



The module's properties consist of private variables to hold the property values and an interface for other objects/code to access those values. Properties can be read/write, read only, or the more rarely used write only.
The AttributeTitle property is defined as ReadOnly because we don't want any other code changing its value. The AttributeValue property is defined as read/write since we do want other code to change its value. I've also defined AttributeTitle and AttributeValue properties for the form to pass this information between the journal module and the form object. The module and form properties do NOT have to share the same name, it just made sense in this case to do so.




Once we specify the part attribute title and retrieve the value, we create our form object, set its properties accordingly, and display it. The code that does this is:


        'create new form object
        Dim myForm As New Form1
        'set form object properties (current part attribute title and value)
        myForm.AttributeTitle = _attributeTitle
        myForm.AttributeValue = _attributeValue
        'display our form
        myForm.ShowDialog()

The form object, which we'll look at in detail momentarily, has three properties defined: AttributeTitle, AttributeValue, and Canceled. The Canceled property will hold a value of True if the user pressed the cancel button, in which case we'll want the journal to end and take no action. If the user pressed OK, we'll assign the value from the form to the part's attribute.




The Form Code

Public Class Form1
 
    Private _frmAttributeTitle As String
    Public Property AttributeTitle() As String
        Get
            Return _frmAttributeTitle
        End Get
        Set(ByVal value As String)
            _frmAttributeTitle = value
        End Set
    End Property
 
    Private _frmAttributeValue As String
    Public Property AttributeValue() As String
        Get
            Return _frmAttributeValue
        End Get
        Set(ByVal value As String)
            _frmAttributeValue = value
        End Set
    End Property
 
    Private _canceled As Boolean = False
    Public ReadOnly Property Canceled() As Boolean
        Get
            Return _canceled
        End Get
    End Property
 
    Private Sub Form1_Load(sender As System.Object, e As System.EventArgs) Handles MyBase.Load
        Label1.Text = _frmAttributeTitle
        TextBox1.Text = _frmAttributeValue
    End Sub
 
    Private Sub btnCancel_Click(sender As System.Object, e As System.EventArgs) Handles btnCancel.Click
        _canceled = True
        Me.Close()
    End Sub
 
    Private Sub btnOK_Click(sender As System.Object, e As System.EventArgs) Handles btnOK.Click
        _frmAttributeValue = TextBox1.Text.ToUpper
        Me.Close()
    End Sub
 
End Class



The form object has its own properties defined. Unlike the module's AttributeTitle property, the form's AttributeTitle property is defined as read/write. This helps make the form more reusable; if you want to customize the code to display the value of a different part attribute, you only need to change it in the module code.




As you can see, the form code is pretty simple and straightforward (helped by the fact that it does no error checking or input validation). When the form loads, the property values are transferred to the label and textbox. The user is free to change the value in the text box and then press OK or Cancel. Once one of the buttons are pressed, the form closes and execution resumes in the journal module. In our case Sub Main displayed the form so it is Sub Main that resumes execution when the form is closed.




If the Cancel button is pressed, the private variable _canceled is updated to True before the form closes. As Sub Main resumes execution, it checks the form's Canceled property (which returns the _canceled value) and simply ends the journal. If the user presses OK, the current value in the text box is copied to the _frmAttributeValue variable before the form is closed. Sub Main takes over, checks the form's Canceled property, sees that the user pressed OK, and assigns the form's AttributeValue property to the part attribute of interest and finally ends the journal as seen in the code below (copied from the journal module code above).




        If myForm.Canceled Then
            'user pressed cancel, exit journal
            Return
        Else
            'user pressed OK, assign value from form to part attribute
            _attributeValue = myForm.AttributeValue
            workPart.SetUserAttribute(_attributeTitle, -1, _attributeValue, Update.Option.Later)
 
        End If




Putting it all together

Ok, so we've created a form and written code to pass information between the form and journal code. How do we get all of this to work together as a journal? One of the limitations of journaling is that all the code must be contained in a single file. One way to accomplish this is to fire up notepad and copy & paste all the code into a new text file. Open a windows explorer and browse to your code project folder. There are three files that you will need to copy into a new file:

  • the module code
  • the form code
  • the form designer code

The module code and form code are fairly obvious, that's the code that we've been directly editing - but what's the 'form designer code'?


The 'form designer code' is a text file description of your form and its controls that the IDE creates as you create and edit your form. While it's possible to edit your form by editing this file, it is a bad idea to do so. Hand editing this file for form changes is a slow, error prone process. It would be better to let the IDE's form designer do what it does best.




Below is the full code listing combining the module, form, and form designer code:


Option Strict Off
Imports System
Imports NXOpen
 
Module Module1
 
    Private Const _attributeTitle As String = "PROJECT"
    Public ReadOnly Property AttributeTitle() As String
        Get
            Return _attributeTitle
        End Get
    End Property
 
    Private _attributeValue As String = ""
    Public Property AttributeValue() As String
        Get
            Return _attributeValue
        End Get
        Set(ByVal value As String)
            _attributeValue = value
        End Set
    End Property
 
    Sub Main()
 
        Dim theSession As Session = Session.GetSession()
        If IsNothing(theSession.Parts.Work) Then
            'active part required
            Return
        End If
 
        Dim workPart As Part = theSession.Parts.Work
        Dim lw As ListingWindow = theSession.ListingWindow
        lw.Open()
 
        Const undoMarkName As String = "NXJ form demo journal"
        Dim markId1 As Session.UndoMarkId
        markId1 = theSession.SetUndoMark(Session.MarkVisibility.Visible, undoMarkName)
 
        Dim myAttributeInfo As NXObject.AttributeInformation
 
        Try
            myAttributeInfo = workPart.GetUserAttribute(_attributeTitle, NXObject.AttributeType.String, -1)
            _attributeValue = myAttributeInfo.StringValue
 
        Catch ex As NXException
            If ex.ErrorCode = 512008 Then
                'attribute not found
            Else
                'unexpected error: show error message, undo, and exit journal
                MsgBox(ex.ErrorCode & ": " & ex.Message)
                theSession.UndoToMark(markId1, undoMarkName)
                Return
            End If
 
        Finally
 
        End Try
 
        'create new form object
        Dim myForm As New Form1
        'set form object properties (current part attribute title and value)
        myForm.AttributeTitle = _attributeTitle
        myForm.AttributeValue = _attributeValue
        'display our form
        myForm.ShowDialog()
 
        If myForm.Canceled Then
            'user pressed cancel, exit journal
            Return
        Else
            'user pressed OK, assign value from form to part attribute
            _attributeValue = myForm.AttributeValue
            workPart.SetUserAttribute(_attributeTitle, -1, _attributeValue, Update.Option.Later)
 
        End If
 
        lw.Close()
 
    End Sub
 
    Public Function GetUnloadOption(ByVal dummy As String) As Integer
 
        'Unloads the image when the NX session terminates
        'GetUnloadOption = NXOpen.Session.LibraryUnloadOption.AtTermination
 
        '----Other unload options-------
        'Unloads the image immediately after execution within NX
        GetUnloadOption = NXOpen.Session.LibraryUnloadOption.Immediately
 
        'Unloads the image explicitly, via an unload dialog
        'GetUnloadOption = NXOpen.Session.LibraryUnloadOption.Explicitly
        '-------------------------------
 
    End Function
 
End Module
 
 
 
 
 
 
 
 
 
Public Class Form1
 
    Private _frmAttributeTitle As String
    Public Property AttributeTitle() As String
        Get
            Return _frmAttributeTitle
        End Get
        Set(ByVal value As String)
            _frmAttributeTitle = value
        End Set
    End Property
 
    Private _frmAttributeValue As String
    Public Property AttributeValue() As String
        Get
            Return _frmAttributeValue
        End Get
        Set(ByVal value As String)
            _frmAttributeValue = value
        End Set
    End Property
 
    Private _canceled As Boolean = False
    Public ReadOnly Property Canceled() As Boolean
        Get
            Return _canceled
        End Get
    End Property
 
    Private Sub Form1_Load(sender As System.Object, e As System.EventArgs) Handles MyBase.Load
        Label1.Text = _frmAttributeTitle
        TextBox1.Text = _frmAttributeValue
    End Sub
 
    Private Sub btnCancel_Click(sender As System.Object, e As System.EventArgs) Handles btnCancel.Click
        _canceled = True
        Me.Close()
    End Sub
 
    Private Sub btnOK_Click(sender As System.Object, e As System.EventArgs) Handles btnOK.Click
        _frmAttributeValue = TextBox1.Text.ToUpper
        Me.Close()
    End Sub
 
End Class
 
 
 
 
 
 
 
 
 
<Global.Microsoft.VisualBasic.CompilerServices.DesignerGenerated()> _
Partial Class Form1
    Inherits System.Windows.Forms.Form
 
    'Form overrides dispose to clean up the component list.
    <System.Diagnostics.DebuggerNonUserCode()> _
    Protected Overrides Sub Dispose(ByVal disposing As Boolean)
        Try
            If disposing AndAlso components IsNot Nothing Then
                components.Dispose()
            End If
        Finally
            MyBase.Dispose(disposing)
        End Try
    End Sub
 
    'Required by the Windows Form Designer
    Private components As System.ComponentModel.IContainer
 
    'NOTE: The following procedure is required by the Windows Form Designer
    'It can be modified using the Windows Form Designer.  
    'Do not modify it using the code editor.
    <System.Diagnostics.DebuggerStepThrough()> _
    Private Sub InitializeComponent()
        Me.btnCancel = New System.Windows.Forms.Button()
        Me.btnOK = New System.Windows.Forms.Button()
        Me.Label1 = New System.Windows.Forms.Label()
        Me.TextBox1 = New System.Windows.Forms.TextBox()
        Me.SuspendLayout()
        '
        'btnCancel
        '
        Me.btnCancel.DialogResult = System.Windows.Forms.DialogResult.Cancel
        Me.btnCancel.Location = New System.Drawing.Point(178, 107)
        Me.btnCancel.Name = "btnCancel"
        Me.btnCancel.Size = New System.Drawing.Size(85, 50)
        Me.btnCancel.TabIndex = 0
        Me.btnCancel.Text = "Cancel"
        Me.btnCancel.UseVisualStyleBackColor = True
        '
        'btnOK
        '
        Me.btnOK.Location = New System.Drawing.Point(66, 107)
        Me.btnOK.Name = "btnOK"
        Me.btnOK.Size = New System.Drawing.Size(85, 50)
        Me.btnOK.TabIndex = 1
        Me.btnOK.Text = "Ok"
        Me.btnOK.UseVisualStyleBackColor = True
        '
        'Label1
        '
        Me.Label1.Location = New System.Drawing.Point(12, 54)
        Me.Label1.Name = "Label1"
        Me.Label1.Size = New System.Drawing.Size(79, 13)
        Me.Label1.TabIndex = 2
        Me.Label1.Text = "Label1"
        Me.Label1.TextAlign = System.Drawing.ContentAlignment.MiddleRight
        '
        'TextBox1
        '
        Me.TextBox1.Location = New System.Drawing.Point(97, 51)
        Me.TextBox1.Name = "TextBox1"
        Me.TextBox1.Size = New System.Drawing.Size(166, 20)
        Me.TextBox1.TabIndex = 3
        '
        'Form1
        '
        Me.AcceptButton = Me.btnOK
        Me.AutoScaleDimensions = New System.Drawing.SizeF(6.0!, 13.0!)
        Me.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font
        Me.CancelButton = Me.btnCancel
        Me.ClientSize = New System.Drawing.Size(284, 176)
        Me.Controls.Add(Me.TextBox1)
        Me.Controls.Add(Me.Label1)
        Me.Controls.Add(Me.btnOK)
        Me.Controls.Add(Me.btnCancel)
        Me.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog
        Me.MaximizeBox = False
        Me.MinimizeBox = False
        Me.Name = "Form1"
        Me.Text = "Form1"
        Me.ResumeLayout(False)
        Me.PerformLayout()
 
    End Sub
    Friend WithEvents btnCancel As System.Windows.Forms.Button
    Friend WithEvents btnOK As System.Windows.Forms.Button
    Friend WithEvents Label1 As System.Windows.Forms.Label
    Friend WithEvents TextBox1 As System.Windows.Forms.TextBox
End Class


Conclusion

We've looked at creating a form and passing information between the form and journal code. The guiding principle is to only use global variables when absolutely necessary; in keeping with this prinicple, we used object properties to pass the information and limit variable scope. As your project complexity grows, it will be beneficial to create your own classes and forms to 'divide and conquer' the programming task at hand.



Next Steps

Modify this journal to edit a part attribute that you often use. Try adding some validation to the user's input. The simple code presented here would allow some strange input, even some characters which may not be allowed in a part attribute! If you have a small number of allowable input values, try using a list box instead of a text box; the user could simply pick the desired value from the list rather than entering their own (probably misspelled) value. Edit this journal to make it your own. Build a more complex input form for your specialized task.


Happy coding!

Comments

How to have 2 FORMS. A button on FORM1 which opens FORM2?

Carlo Tony Daristotile

private void button1_Click(object sender, EventArgs e)
{
Form1 f1 = new Form1();
Form2 f2 = new Form2();
this.Hide();
f2.ShowDialog();
this.Close();
}
Santosh

Do you have a simple example similar to the code above "Putting it all together" , but with the 2 forms ?

Carlo Tony Daristotile

Suppose i have taken two forms:
Fom1 contains one label(username) & one button
Form2 contains one textbox, & one button
after clicking button1 of form1 , form2 will popup whatever you are going to type in textbox of form2 will reflect in label of form1
for this go in form2designer2.cs change this (public System.Windows.Forms.TextBox textBox1;) in public
then click button of form2 & write this.close();
now click button event of form1 & write below code
private void button1_Click(object sender, EventArgs e)
{
Form2 fr = new Form2();
fr.ShowDialog();
label1 .Text =fr.textBox1.Text;
}

Santpsh

I noticed that when running this journal, the attribute value updates but the drawing note (that links to that attribute) doesn't update until I update the drawing. If, however, I go to File > Properties and edit the value, it updates immediately. 1) why the difference? 2) can the journal be edited to update the drawing upon completion?

Matt

If I want to hand over a 3 x 3 Array from Form1 to the journal and reverse. What is the best solution? Public Array? Thank you and best regards

A public variable would certainly work in this case.

The programming "best practice" would be to limit the variable scope as much as possible. One way to do this is to use private variables and use properties to get/set the values as demonstrated in the code in the article. Using properties provides for some safeguards but is probably overkill if your journal is small and simple.

Hello,

I compressed the array into one variable like you suggested and splitted it after handover. This works perfectly. Thank you for your help so far.

May I ask you an other question? I would like to pimp my form with a background image. Is this possible? A reference to resources in the form designer is created automatically:

Me.BackgroundImage = CType(resources.GetObject("$this.BackgroundImage"), System.Drawing.Image)

What data and where do I have to include this data after "Putting it all together" that the image will work?

Best regards

I think that you can only use the "resource file" if you compile your code (only possible if you have an NX author license).

However, even if you are running your code as a journal, you should be able to change the background image of your form. Save the image in a convenient location and in your form load routine, load the image into the form background.