Getting Started with Windows Forms
Ken Getz, MCW Technologies
Paul Sheriff, PDSA, Inc.
Summary: This document walks you through the creation of a simple MDI application created using Microsoft Visual Basic .NET; the focus is not on writing code, but on getting you started working with the Microsoft Windows Forms Designer, and on showing you how to do some things that you were comfortable with in Microsoft Visual Basic 6 that aren't so obvious any more. (15 printed pages)
- Start working with Microsoft® Windows® Forms
- Create a simple MDI application in Microsoft Visual Studio® .NET
- Work with menus in Windows Forms applications
- Add pop-up dialog boxes to Windows applications
- Manage events for multiple controls
The following should be true for you to get the most out of this document:
- You're a Microsoft Visual Basic® developer, and know how to create forms
- You know Microsoft Visual Basic .NET
- You want to create MDI forms
Learning Windows Forms Designer
Creating MDI Applications
Creating a Base Form
Anchor the Buttons to the Bottom Right
Inheriting from the Template Form
Displaying Forms Programmatically
Finishing Up the MDI Child Form
Creating Modal Dialog Boxes
Emulate Control Arrays
What's Different From Visual Basic 6.0?
About the Authors
Although Microsoft Visual Studio .NET makes it much easier for developers to create Web-based applications than did previous versions of Visual Basic, there will still be many times when you will want to create Microsoft Windows-based applications that run, at least partially, on the client computer. Rather than relying on the Microsoft Visual Basic-specific forms you've used for several versions of Visual Basic, Visual Studio .NET provides a new Windows-based forms package. All of the .NET languages share the same forms designer, but the Windows Forms you create using Visual Studio .NET can be inherited by projects written in other languages and you can use the same tools, no matter which .NET language you're using. You're no longer restricted to only the Visual Basic environment, so it's easy to migrate your skills from one language to another.
What are the real differences between the Visual Basic 6.0 forms and the Visual Studio .NET Windows Forms? Basically, you'll find yourself with many new built-in controls, a new designer for the forms themselves, many changed, added, and removed properties, and far less "magic" behavior—in this version, the code that hooks up properties, event procedures, and other "housekeeping" is all provided as code within the form's module.
Rather than list all the new features of Windows Forms and the associated controls (you'd be reading for awhile—there are a lot of new and modified features), this document will walk you through the creation of a simple MDI application created using Visual Basic .NET. This document doesn't focus on writing code: it's aimed at getting you started working with the Windows Forms Designer, and shows how to do some things that you were comfortable with, in Visual Basic 6, that aren't so obvious any more. Please note that the order of the features discussed here isn't significant—you'll simply see a barely working demo, and this document will discuss the features used to work through the steps. You'll learn the most about Windows Forms by following the steps provided here to get yourself started with this new forms package.
For this sample walkthrough, you create a simple MDI application. To start, create the new project: Select Visual Basic Projects, and then the Windows Application project type. Set the name of the project to MDICustomerApp.
When you create a standard Windows application, Visual Studio .NET creates a project that includes a single form. This form will become your MDI parent form. In the Properties window, modify the form's caption by setting the Text property for the form to Customer Tracking Application.
In order to make this form be the MDI container for the rest of your application, set the form's IsMDIContainer property to True. (Unlike Visual Basic 6 projects, Visual Studio .NET projects can contain more than a single MDI parent window.)
In the Solution Explorer window (on the View menu, click Solution Explorer to make this visible if it's not already), select Form1.vb, right-click, and on the shortcut menu, click Rename (or simply press F2). Rename this form to frmMain.vb.
Next, add a set of menus to your main form. Double-click the MainMenu tool in the Toolbox window to add a new object named MainMenu1 to the form tray. (Unlike Visual Basic 6, menus for a form are treated as a control on that form, and controls that aren't part of the form—menus, timers, and other invisible controls—are placed into a special area, the form tray displayed at the bottom of the form at design time.) The good news: the menu editor that Visual Basic has included for years is gone. Visual Studio .NET provides a graphical menu editor that's intuitive and is a breeze to use. At the top of the MDI parent form you should see a small box with the text Type Here. Click this area and type &File. Press Enter to move to the next menu item. Type &New Customer. Press Enter one more time and type E&xit.
At the same level and to the right of the File menu, you'll see another small box with the text Type Here. Click that menu area, and type the following menu items: &Window and then below the Window menu item, &Cascade, Tile &Horizontal, Tile &Vertical, and &Arrange Icons.
After you create all the menu items, you should set the Name property for each. Instead of clicking on each menu item, one at a time, and then moving over to the Properties window to set the Name property, Visual Studio provides a shortcut: Right-click a menu item, and then select Edit Names from the context menu. You can simply click each menu item, and set the Name property on each menu. This is much quicker than using the Properties window to accomplish the same task. Use the following names for your menu items: mnuFile, mnuFNew, mnuFExit, mnuWindow, mnuWCasade, mnuWTileHorizontal, mnuWTileVertical, mnuWArrange.
To add code to react to the menu items' Click event, double-click each item in turn and add code to call the LayoutMDI method of the main form. (The LayoutMDI method replaces the Arrange method you may have used in Visual Basic 6.) For example, the code called from the Cascade menu item's Click event might look like this:
Private Sub mnuWCascade_Click( _ ByVal sender As System.Object, _ ByVal e As System.EventArgs) _ Handles mnuWCascade.Click Me.LayoutMdi(MdiLayout.Cascade) End Sub
Repeat for each of the other items on the Window menu.
Note Look carefully at the mnuWCascade_Click event procedure. You'll see that there's a Handles clause, which wasn't available in Visual Basic 6 event procedures. The Handles clause takes care of hooking up the event procedure "plumbing". As opposed to Visual Basic 6, where the name of the procedure itself indicated to the run-time event manager exactly which procedure should be called in reaction to an event, in the new Windows Forms, the name is inconsequential—the Handles clause hooks up event handlers. As you'll see later, you can also declaratively add event handlers (as many as you need, for a specific event), using the AddHandler statement as well.
If you are going to create several standard data entry forms, you will probably want to come up with a common look and feel for all of your forms. Visual Studio .NET provides visual inheritance: the ability to create a base form, and then inherit its visual features in other forms. (Inherited forms include all the properties, controls, and event procedures provided by the base form.) Changes you make to the base form ripple through all the forms that inherit from the base form. Create a base form that looks like Figure 1 by following the steps outlined below.
Figure 1. Creating a base form
By creating a base form for your application and including all the controls that you're going to want to include on all the forms that inherit from this form, you can save yourself a great deal of time creating forms for your application. In this case, every data entry form will require these buttons, anchored to the bottom right edge of the form.
On the Project menu, click Add Windows Form. Select the Windows Form template named frmDataEntry.vb. In the Toolbox window, double-click Button to add a button control to this new form. Move the button to the bottom-right corner of the form.
In the Properties window, set the Text property for the button to Save, and set the Name property to btnSave. Click once on this button on the form to give it focus. Copy the button to the Clipboard, and then paste a new copy back onto the form. Visual Studio will place this new button directly on top of the old button: you'll need to drag the new button to its new location, immediately to the left of the original button. Set the new button's Text property to Cancel, and its Name property to btnCancel.
You may want to add code to a base form, so that all forms that inherit from the form also inherit the behaviors you've added. To add a procedure that sets the enabled state of the buttons, press F7 (or right-click and choose View Code on the shortcut menu) to load the form's module.
It's not this document's charter to dig into the code associated with a form, but for now, simply scroll to the bottom of the form's module, and add the following procedure immediately above the End Class you'll find at the bottom:
Public Sub HandleSave( _ ByVal blnEnableSave As Boolean) btnSave.Enabled = blnEnableSave btnCancel.Enabled = blnEnableSave End Sub
You should note, as you investigate the code associated with a form, a collapsed region named "Windows Form Designer generated code" in the form's module. This section contains code that creates the form's properties and hooks up the form's event procedures. The forms designer always overwrites much of this section, when you make changes at design time. You may find it interesting to peruse this code, so click + to open the section.
Next, add code to call the HandleSave procedure when any form based on this form opens. To do this, in the New method for the form (the procedure that gets called as the module is instantiated, hidden away within the previously collapsed region), add the following procedure call immediately following the
Add any initialization after the InitializeComponent() call comment in the code:
If you're like many Visual Basic 6 developers, you've written code to anchor controls to the bottom-right edge of a form. You want users to be able to resize the form, but you want certain controls on the form to always maintain their placement, relative to the bottom and/or the right edge of the form. Throw away the code! Visual Studio .NET provides a simple solution—the Anchor property. This property allows you to anchor a control to any, or all, of the form's edges. Anchoring to the bottom and/or right edge causes the control to float as you resize the form. Anchoring to the top and/or left edge causes the control to resize as you resize the form. You'll want to experiment with this new property to get accustomed to its effect on your controls. (Also, review the Dock property, which allows you to dock a control to any of the form edges or all the form edges, effectively filling the entire form.)
On your standard data entry form you probably want the buttons to stay in the lower right-hand corner of the form. To accomplish this, highlight the buttons, and set the Anchor property to Bottom, Right. To do this, find the Anchor property in the Properties window, then click the drop-down arrow. Unselect the bars on the top and left, then select the bars in the bottom and right, as shown in Figure 2.
Figure 2. The Anchor property
As shown in Figure 2, the Anchor property (and its cousin, the Dock property), allow you to anchor and dock controls to edges of your forms without having to write a line of code. By selecting options from the graphic menus, you can select the behavior you need.
Once you've created a base form, you can create new forms that inherit the design and behavior of the template. Before you can inherit from the form, however, you must compile the project. Do that now: On the Build menu, click Rebuild All to compile everything. Now, you can inherit from your data entry form.
To create the new inherited form, on the Project menu, click Add Inherited Form. Enter frmCustomer.vb as the name for this new form. When prompted for the name of the form to inherit from, select frmDataEntry and click OK.
You should see the new form in the form designer window, including the command buttons you created in the previous section. You can resize this window by dragging the lower right-hand corner, and the command buttons will move with the screen.
You can now start building your customer data entry form around these command buttons. The next few paragraphs describe some interesting controls, new in the Windows forms package that you'll find useful. We'll only discuss some of the controls on this form here, but Figure 3 shows the sample data entry form, with all its controls in place.
Figure 3. Our finished data entry form
This sample form includes some new controls and features (discussed in a later section):
The control for entering the City name is a DomainUpDown control, which allows you to scroll through a list of possible values, much like a normal numeric UpDown control. The button to its right pops up a form that allows you to add new items to the list. A later section works through the use of this control in more detail.
The sample form includes ContextMenu and ToolTip controls, which allow you to add context menus and tool tips to other controls on the form. They appear in the tray area at the bottom of the form designer. You'll add these later.
To further investigate the anchoring abilities in Windows Forms, set the Anchor property for txtMemo (the text box to the right of the Memo label) to "Top, Bottom, Left, Right". This anchors the top and left sides so they always stay the same distance from the top and left sides of the form. In addition, this forces the bottom and right edges of the text box to maintain a fixed distance from the bottom and right edges of the form—in effect, causing the text box to resize itself to allow for more visible area as you resize the form.
Tip You may want to limit the minimum and maximum dimensions of the form. You can use the MinimumSize and MaximumSize properties of the form to manage how large and small the user can change your form. One simple way to set these properties is to visually size the form to the smallest size you want to allow, and then copy the coordinates from the form's Size property to its MinimumSize property. Repeat for the MaximumSize property, after visually sizing the form the largest you want to allow.
Now that you've created the main MDI parent form and the child form as well, it is time to create an instance of the child form. When you click the New Customer menu on the MDI parent form, you'd like an instance of frmCustomer to appear. This works just as in Visual Basic 6 (you must create a new instance of the form, then show it), except that you must also tell the child form who its parent is. Because Visual Studio .NET allows you to have multiple MDI parent forms in the same project, this step is required. You can modify the Click event of mnuFNew (double-click New Customer on frmMain), adding this code:
Private Sub mnuFNew_Click( _ ByVal sender As System.Object, _ ByVal e As System.EventArgs) _ Handles mnuFNew.Click Dim frm As New frmCustomer() frm.MdiParent = Me frm.Show() End Sub
Note In this code, instantiating an object requires that you include parentheses because Visual Basic .NET allows you to pass parameters to the constructor of any object. This example doesn't happen to pass any parameters, so it includes empty parentheses.
Imagine that you'd like to allow users to select a city from a provided list of cities, scrolling using up and down arrows. To accomplish this, you can place a DomainUpDown control on your form. You've probably used the standard Visual Basic UpDown control in your applications, but that control doesn't handle text data. If you want to provide a list of text values you can move up and down through, you had to write code to handle the activity yourself. On Windows Forms, you can use the DomainUpDown control to provide this behavior.
Add a DomainUpDown control to frmCustomer, and set its Name property to dupdCities. To provide the list of cities, select the Items property in the Properties window, click the … button, and enter the list of cities in the String Collection Editor window. You can, if you'd like, modify this list or provide it entirely in the code for your application. If you set the Sorted property to True, the list will be sorted. Set the ReadOnly property to True. This allows you to select items in the list by typing the first letter. (Leaving this property False allows you to enter new items, but it performs no lookup as you type.) Add a button to the right of the DomainUpDown control named btnCities, which will allow you to enter a new city and state combination. You'll hook up the code for this button later in the article.
Visual Studio .NET makes it simple to add context menus to your forms. In this case, you'll add a simple menu containing Cut, Copy, and Paste menu items, using the new ContextMenu control. Select the ContextMenu control in the Toolbox window, and double-click to place an instance on the form. You'll note that the control doesn't display directly on the form, but in the tray area underneath, as shown in Figure 4.
Figure 4. Adding menu items
As shown in Figure 4, once you've added a ContextMenu control to your form, you can add the menu items you need, just as when you're creating a main menu, you can work with the items visually.
With the new ContextMenu control (named ContextMenu1) selected, modify the menu at the top of the form, adding &Cut, &Copy, and &Paste menu items. Just as before, you can select one of the menu items, right-click, and select Edit Names from the context menu to enter name-editing mode. Set the names to mnuCut, mnuCopy, and mnuPaste. (We'll leave the code for these menu items up to you.)
To assign the context menu to various controls, you can select all the editing controls on your form, and set the ContextMenu property to ContextMenu1. Then, when you run the form, right-click on any of these controls to display the new context menu you've just created.
You'll note that none of the controls on the form supply a ToolTip property. In Visual Studio .NET, you must add a ToolTip control to the form—once you've done that, you'll see a ToolTip on ToolTip1 property (where ToolTip1 is the name of the ToolTip control you've just placed on the form) for each control on the form.
Try placing a ToolTip control on your form, and note that it shows up in the tray area, along with the ContextMenu control. Then, set the ToolTip on ToolTip1 property for some of the controls on your form. When you run the form and hover the mouse over the controls, you should see the tool tip text you've assigned.
After you've laid out your form, you might want to set the tab order of the various controls. Previous versions of Visual Basic required you to either set each TabIndex property value manually, or to purchase an add-in to accomplish this task visually, but Visual Studio .NET provides its View menu/Tab Order menu item that handles this task. To set the tab order, on the View menu, click Tab Order, and then click the controls on the form in the order in which you'd like them to be visited when the user presses Tab. Figure 5 shows the sample form in the middle of having its tab order edited.
Figure 5. Setting the Tab Order for each control on your forms
As shown in Figure 5, on the View menu, click Tab Order to set the TabOrder property for each control on your forms. As you click on each item in turn, the designer updates each control's TabOrder property with the next available consecutive value. It would be nice to have a "set tab order automatically, from upper-left to lower-right" option, but there isn't one.
As promised earlier, you can use Windows Forms' features to make it easy to create and work with modal dialog boxes in Visual Studio .NET. In this section, you'll create a dialog form that allows you to enter a new city and state.
On the Project menu, click Add Windows Form to add a new form. Supply the name frmCityState.vb for the new form. Set the Text property for the new form to Enter New City and State, and set the BorderStyle property to FixedDialog. Add labels and text boxes (named txtCity and txtState) so that a user could enter city and state values. Finally, add two command buttons (named btnOK and btnCancel), with appropriate Text properties for each. Figure 6 shows how the form should look when you're done.
Figure 6. The completed version of frmCityState
Select the form itself, and in its Properties window, set the AcceptButton property to btnOK and the CancelButton property to btnCancel. (The AcceptButton property replaces the Visual Basic 6 Default property, and the CancelButton property replaces the old Cancel property. Now, these are appropriately properties of the form itself.) These properties affect what happens when you press Enter or Escape while the form has the focus—pressing Enter acts as if you had clicked the button set in the AcceptButton property, and pressing Escape acts as if you'd clicked the button in the CancelButton property.
To make it easier for you to create MessageBox-like forms, Visual Studio .NET provides a DialogResult property for each button on the form. If you use the ShowDialog method of a form to display the form modally, click a button on the form that has its DialogResult property set, the return value from the ShowDialog method (you can also retrieve the form's DialogResult property, at run time) will contain the DialogResult property value of the button you clicked. Once you've set the DialogResult property for a button, clicking that button will close the dialog form, so you needn't write that code explicitly. If you leave the property at its None default, clicking the button will run its Click event procedure and won't close the form. Set the DialogResult property for btnOK to OK and for btnCancel, to Cancel.
Once you've set up your dialog box form, it's time to make it do its job. Add the following code to the Click event of the button immediately to the right of dupdCities, on frmCustomer:
Private Sub btnCities_Click( _ ByVal sender As Object, _ ByVal e As System.EventArgs) _ Handles btnCities.Click Dim strCity As String Dim strState As String ' Code removed here. Dim frmCityState As New frmCityState() If frmCityState.ShowDialog() = _ DialogResult.OK Then strCity = frmCityState.txtCity.Text strState = frmCityState.txtState.Text If strCity <> String.Empty AndAlso _ Not dupdCities.Items.Contains(strCity) Then dupdCities.Items.Add(strCity) End If If strState <> String.Empty AndAlso _ Not cboStates.Items.Contains(strState) Then cboStates.Items.Add(strState) End If End If End Sub
This code takes care of several issues. First of all, it uses the ShowDialog method of the dialog box form, frmCityState, to display the form modally, waiting for the form to be dismissed. Because you set the DialogResult property of the two buttons on the form, clicking either one will dismiss the form:
Dim frmCityState As New frmCityState() If frmCityState.ShowDialog() = _ DialogResult.OK Then ' Code removed here. End If
Next, the code checks the return value of the ShowDialog method. If the return value is DialogResult.OK (indicating that you selected OK on the dialog box form), the code retrieves the city and state values from the form, and determines whether it should add the values to their appropriate controls. In each case, if the string isn't an empty string, and if the control doesn't already contain the text, the code adds the item to the control:
If strCity <> String.Empty AndAlso _ Not dupdCities.Items.Contains(strCity) Then dupdCities.Items.Add(strCity) End If If strState <> String.Empty AndAlso _ Not cboStates.Items.Contains(strState) Then cboStates.Items.Add(strState) End If
This code also contains a few new wrinkles for Visual Basic 6 developers. First, String.Empty provides a field (a constant value) that represents an empty string. Using this field, rather than comparing strings to "" each time you need to check the value of string can save you both memory and time. The code fragment also uses the new AndAlso operator, provided by Visual Basic .NET. In previous versions of Visual Basic, you used the And operator to verify that both conditions were true (the string wasn't empty, and it wasn't already in the list) before adding the item to the list. The And operator in Visual Basic 6 evaluated both expressions before determining the logical result value. Other languages, such as C++, have always incorporated logical "short-circuiting"—an optimization that both saves time and programming logic. (If the first operand of the And operator is False, the second operand's value doesn't matter—the return value will be False. That's just the way the And operator works.) Because there's no need to check to see if the item exists in the list, if it's an empty string, you can use the new AndAlso operator in this case. Here, if the string retrieved from the dialog box form is empty, the code won't bother attempting to find the string within the ComboBox or DomainUpDown control.
Tip The OrElse operator provides the same sort of logical short-circuiting as the AndAlso operator. If either operand of an Or operator is True, the second operand's value doesn't matter—the return value will be True. The OrElse operator acts like the Visual Basic Or operator, except that it doesn't bother evaluating the second operand if the first is True.
The controls on the MDI child form need to be able to call the HandleSave method of the form from which it was inherited, and it must call the procedure each time any one of the controls on the form changes its value. This presents two problems: how do you call a procedure in your base class, and how do you have multiple controls call the same procedure?
The answer to the first question is simple: You have access to Public, Friend, or Protected procedures in the form's base class using the MyBase keyword. Add a procedure to frmCustomer's module, like this (the parameters are required, as you'll see in a moment), to call the parent form's HandleSave method:
Protected Sub HandleTextChanged( _ ByVal sender As Object, _ ByVal e As System.EventArgs) MyBase.HandleSave(True) End Sub
To have multiple controls call the same event procedures in Visual Basic 6, you might have created a control array, calling the HandleTextChanged procedure from the Change event of the control array. Of course, control arrays are limited to containing only controls of a single class, so it wouldn't help much for this form that contains TextBox, DomainUpDown, and ComboBox controls. Changing the value of any of these controls must end up calling the HandleTextChanged method.
Instead of creating a control array, you can create an event handler and assign it to react to one or more events of multiple controls. Given the HandleTextChanged procedure you've already added, you can add event handlers that all refer to this same procedure. To do this, you can add a Handles clause to the HandleTextChanged procedure for each event you wish it to handle.
For TextBox controls, you want to react to the TextChanged event, and for the DomainUpdown control, the SelectedItemChanged event. For the ComboBox control, it's the SelectedIndexChanged event to which you must react. In each case, you need to call the HandleTextChanged method, which will set the enabled state of the buttons on the form.
To add a Handles clause to the HandleTextChanged procedure, modify the procedure so that it looks like this:
Protected Sub HandleTextChanged( _ ByVal sender As Object, _ ByVal e As System.EventArgs) _ Handles cboStates.SelectedIndexChanged, _ dupdCities.SelectedItemChanged, _ txtCompany.TextChanged, txtFirst.TextChanged, _ txtLast.TextChanged, txtMemo.TextChanged, _ txtStreet.TextChanged, txtZipCode.TextChanged MyBase.HandleSave(True) End Sub
After adding the code, run the project. Create a new customer form, and modify any control's value on the form. As soon as you do, you should see the Save and Cancel buttons enabled. No matter which control you modify, the result should be same, because you're calling the same procedure in reaction to different events for the various controls.
There are many new built-in controls, including a new designer for the forms themselves, many changed, added, and removed properties, and, in this version, the code that hooks up properties, event procedures, and other "housekeeping" is all provided as code within the form's module.
You created a new MDI project, using the .NET-created form as a container for the rest of the application. You looked at the elements of the new form including defining menu navigation, setting a base form with inheritable behaviors as a standard for building other forms within the project, defining a form's appearance using anchoring and docking, providing context menus to facilitate data entry, adding ToolTips and modal dialog boxes, and emulating Control arrays using event handlers.
Ken Getz is a senior consultant with MCW Technologies and splits his time between programming, writing, and training. He specializes in tools and applications written in Microsoft Access, Visual Basic, and the rest of the Microsoft Office and Microsoft BackOffice suites. Ken has co-authored a book entitled ASP.NET Jumpstart with Paul D. Sheriff. Ken is co-author of several books, including Access 97 Developer's Handbook with Paul Litwin and Mike Gilbert, Access 2000 Developer's Handbooks with Paul Litwin and Mike Gilbert, Access 2002 Developer's Handbooks with Paul Litwin and Mike Gunderloy, Visual Basic Language Developer's Handbook with Mike Gilbert, and VBA Developer's Handbook with Mike Gilbert (Sybex). He co-wrote training materials and teaches for AppDev. Ken is a frequent speaker at technical conferences, and has spoken at the Microsoft Tech*Ed conferences since 1994. Ken is a Technical Editor for Access/VB/SQL Advisor magazine and Contributing Editor for Informant Communication Group's Microsoft Office Solutions magazine.
Paul D. Sheriff is the owner of PDSA, Inc., a custom software development and consulting company in Southern California. Paul is the MSDN Regional Director for Southern California, is the author of a book on Microsoft Visual Basic 6, Paul Sheriff Teaches Visual Basic, and has produced over 72 videos on Visual Basic, Microsoft™ SQL Server, .NET and Web Development for Keystone Learning Systems. Paul has co-authored a book with Ken Getz entitled ASP.NET Jumpstart. Visit the PDSA, Inc. Web site (www.pdsa.com) for more information.
About Informant Communications Group
Informant Communications Group, Inc. (www.informant.com) is a diversified media company focused on the information technology sector. Specializing in software development publications, conferences, catalog publishing and Web sites, ICG was founded in 1990. With offices in the United States and the United Kingdom, ICG has served as a respected media and marketing content integrator, satisfying the burgeoning appetite of IT professionals for quality technical information.
Copyright © 2002 Informant Communications Group and Microsoft Corporation
Technical editing: PDSA, Inc.