In a previous post I covered several issues I ran into with the vista rdp client. I have a few more problems that have pushed me to write my own RDP client. Its more a wrapper around the ActiveX control so it will require RDP 6.0 (and .NET 2.0) to be installed. But it does solve my problems.
Here are the features I want to include
Disable close button
Fix Windows key sticking
Allow saved passwords inside RDP file
Encrypted RDP files
Custom title bar text
Single sign on (v2)
I have every feature working except the single sign on. I am going to bounce that feature to a later release, but I do have a solution worked out for it.
My app can read any RDP file. I only implement a subset of the options, but I can use my existing RDP files. I did implement all the settings that we change. I also added a few new ones that control the new features I added. "disable close button:i:1" and "disable close button message:s:" are 2 examples of added settings.
To disable the close button, I just catch the close event and display a customizable message if the connection is still active. The return them to the form. This solves the issue we have with open record locks on disconnected sessions from people clicking the X to close.
The custom wrapper has the ability to read a password from the rdp file and use it in the connection. This allows me to publish shortcuts with generic usernames and passwords in them again.
I use RDX extensions to indicate encrypted files. I use TripleDES to protect the files from user changes. I have places that I don't want people messing with the settings. Also good for when the password is saved in the file. A custom RDX file editor is provided to my technicians that would need to make changes to the files.
And by default, the Windows key does not stick. That was a nice side effect.
On deployment, I associate RDP and RDX run commands with my custom application. The icon is unchanged and my user experience does not change much.
Some problems you just can't search on. Here are some I wish were more searchable and this blog is my attempt to make that happen.
Sunday, December 30, 2007
Saturday, December 29, 2007
Raise events on main form from different thread.
I just saw this post on Nicola Delfino's blog. I run into this same thing that he provides the solution for.
Update WinForm interface from a different thread
Well, this is a typical issue when you have a thread that works (i.e. a Workflow) and a UI that needs to be updated.
Let assume that you have a WinFom and you need to update its windows Title from another thread. The other thread needs to call "UpdateTitle" public method of current Form instance.
In order to obtain that it works you need to implement "UpdateTitle".
See his original post for the code sample:Update WinForm interface from a different thread
More information on Control.InvokeRequired Property on MSDN.
Update WinForm interface from a different thread
Well, this is a typical issue when you have a thread that works (i.e. a Workflow) and a UI that needs to be updated.
Let assume that you have a WinFom and you need to update its windows Title from another thread. The other thread needs to call "UpdateTitle" public method of current Form instance.
In order to obtain that it works you need to implement "UpdateTitle".
See his original post for the code sample:Update WinForm interface from a different thread
More information on Control.InvokeRequired Property on MSDN.
Friday, December 28, 2007
Issues with mstsc rdp 6.0
The change to the new Vista RDP client can be a drastic one. A lot of things have changed. Several changes break existing functionality that many people relied on.
Windows key sticking:
If you use the windows key + L to lock your machine while working in a remote session, the windows key becomes stuck. When you return to your session, pressing any key that has a combination with the windows key will activate. E for explorer, L for lock, ect... The solution is to use ctrl +alt +del, or move focus away from the RDP session before locking the computer.
Default domain is wrong:
At some point all of our mstsc client forgot what domain they were connected to. Even though my users logged into the local machine with the domain account and tried to remote into a domain terminal server. They tried to authenticate with the machine name as the domain. The solution is to add the domain to the username when typing it in.
Passwords don't save to the rdp file anymore:
You cannot distribute a RDP file with a password. So the user must know the password to use the rdp shortcut.
Login prompt changed:
Not only is it client side, it also has a different look. You can revert it back by adding a special line into the .RDP file: enablecredsspsupport:i:0
Can only save one username/password per server:
This prevents you from creating 2 shortcuts with 2 different usernames to the same server. I read that you can add a second rdp listening port and point one of those to the new port number to work around this one.
At face value, these look like minor issues.
But the amount of support calls and issues this created in our environment tied us up for a long time. All of them user related. Technology is great when its transparent to the users (and does not generate calls to support staff).
We have a good number of users that cant figure out the difference between username passwords and email passwords. (I cant tell you the number of times we have had someone reboot to fix a problem only to have them call back to tell us they cant get the password to work anymore. ) The change that no longer defaults the domain, the change that sticks the windows key, and the different look of the login screen were all drastic changes to them.
We also have a special application that we deploy using RDP. We create a unique account for each computer that connects to it. We would save the password to the RDP so every user uses the same RDP on that machine. Now we have to have every possible user login, then give us the keyboard to enter the password. Not only is that a huge support drain, its an annoyance to our users. (especially to our remote sites that we visit once every other week).
At one point we added a second app to the servers and tried to create a second shortcut for that. Because the hostname was the same, it was only keeping the login details of one username. (we used a different username to access a different published app.)
The sticking window key compounds the problems. Our users don't know the password to that special application RDP connection. So when they get the windows key stuck and press the L key, the remote session gets locked. We get the call and have to remote into that session and enter the password for them.
Our primary solution that works wonders was to upgrade the Vista 6.0 version to the XP 5.2 version. That upgrade fixed all of those problems.
A second solution that I am about done with is to write my own client wrapper for the RDP 6.0 control. We have other issues that I can't solve any other way, and it solves these issues too. I will write up more details on that solution later.
Wednesday, December 26, 2007
Why does my shared clipboard not work?
I found an exelent write up on Terminal Services Team Blog titled Why does my shared clipboard not work?
I used to have an issue where my clipboard would stop working. I would eventualy have to reboot my computer to fix it. I would work with remote desktop alot and the mstsc was the cause of my problems. That write up explains exactly how the clipboard would lock up.
I uncovered a program that would show me the process containing the lock and allow you to kill it. I would love to link that program, but I cant track it down. I did find another one that a microsoft MVP posted that I think does the same thing. Here is a copy of that post.
Hassan,
This has to do with some application that's having a lock over the Windows Clipboard. David Candy's application should determine the Process that's causing the problem. Download GetOpenClipboardWindow.zip from here: http://windowsxp.mvps.org/temp/GetOpenClipboardWindow.zip Unzip and run the tool. Post back what it reports. For best results, run this utility during the time you encounter the Copy<=>Paste problem.
-- Ramesh, Microsoft MVP
Windows XP Shell/User
In the end, upgrading to the RDP 6.0 client also fixes this problem in mstsc. Now that I have upgraded it, I no longer have this issue.
Monday, December 24, 2007
No accessible 'Main' method with an appropriate signature was found
I was working in a project that was a windows application, but I decided to change it to a console application instead. I changed the type to console and selected Sub Main as my startup object. When I compiled it I got the fallowing error: Error 54 No accessible 'Main' method with an appropriate signature was found.
I expected problems because I knew I did not have the Sub Main defined in my project. I added a class file and entered the fallowing code:
Several examples I saw had main defined with (ByVal args() As String) as the parameter. One thread on the subject indicated that you can add a module instead of a class. Here is the syntax for that.
You will notice that the shared keyword is left off inside the module.
I expected problems because I knew I did not have the Sub Main defined in my project. I added a class file and entered the fallowing code:
Public Class Main
Public Shared Sub Main()
Application.Run(New Form1)
End Sub
End Class
Several examples I saw had main defined with (ByVal args() As String) as the parameter. One thread on the subject indicated that you can add a module instead of a class. Here is the syntax for that.
Module Main
Public Sub Main(ByVal args() As String)
Application.Run(New Form1)
End Sub
End Module
You will notice that the shared keyword is left off inside the module.
Saturday, December 22, 2007
Mapping a drive with custom name
When mapping a drive, the full path of the share is displayed as the name of the drive. We have some very long path names and didn't want our users to see the details they didn't need. So I had to write a script to give those drives custom names.
Our login scripts contained lots of net use statements in *.BAT or *.CMD files. The command looked like this:
and it would show in explorer like this:
My goal was to make a script just as easy to use at NET USE. That is exactly what I ended up with. The new command looks like this:
and it would map like this:
By making the command simple the scripts are easy to read and modify by others on my team. Now that you know what we are doing, lets take a look at the code needed to make this work.
We are going to save this code with the name Map.vbs. Its a VBScript file executed by the windows scripting host. To make the script more usable, it needs to read the variables from the command line. The first 3 lines populate the variables we will use later.
The next 2 lines I am going to show you display how to map the drive.
This will map the path strPath to the drive strDriveLetter. The true as the 3rd param is the same as using the /PERSIST param of NET USE. It saves the mapping in the profile.
Now we need to change the name of the mapping. This takes an existing share by drive letter and gives it a custom display name. This was one of the hardest pieces of code to find. The next 2 lines do just that.
If you put that all together you have a bare bones script to map a drive and rename it from the command line via a VBScript. In my next post, I plan on going more in depth into my map.vbs. I will show you how to check for an existing mapping on that letter and all the validation I added.
Our login scripts contained lots of net use statements in *.BAT or *.CMD files. The command looked like this:
NET USE H: //server/share/directory1/directory2/directory3
and it would show in explorer like this:
directory3 on //server/share/directory1/directory2 (H:)
My goal was to make a script just as easy to use at NET USE. That is exactly what I ended up with. The new command looks like this:
Map.vbs H: //server/share/directory1/directory2/directory3 "My directory3"
and it would map like this:
My directory3 (H:)
By making the command simple the scripts are easy to read and modify by others on my team. Now that you know what we are doing, lets take a look at the code needed to make this work.
We are going to save this code with the name Map.vbs. Its a VBScript file executed by the windows scripting host. To make the script more usable, it needs to read the variables from the command line. The first 3 lines populate the variables we will use later.
strDriveLetter = WScript.Arguments(0)
strPath = WScript.Arguments(1)
strDisplayName = WScript.Arguments(2)
The next 2 lines I am going to show you display how to map the drive.
Set objNetwork = CreateObject("WScript.Network")
objNetwork.MapNetworkDrive strDriveLetter, strPath, true
This will map the path strPath to the drive strDriveLetter. The true as the 3rd param is the same as using the /PERSIST param of NET USE. It saves the mapping in the profile.
Now we need to change the name of the mapping. This takes an existing share by drive letter and gives it a custom display name. This was one of the hardest pieces of code to find. The next 2 lines do just that.
Set oShell = CreateObject("Shell.Application")
oShell.NameSpace(strDriveLetter & "\").Self.Name = strDisplayName
If you put that all together you have a bare bones script to map a drive and rename it from the command line via a VBScript. In my next post, I plan on going more in depth into my map.vbs. I will show you how to check for an existing mapping on that letter and all the validation I added.
Tuesday, December 18, 2007
.NET accessing embeded files in your project
I had some files that I wanted to embed into my project and access in code. I needed both images and xml files.
First you have to add the files to the project. I create the images in paint before importing them. The xml files were created within Visual Studio. Things started to look a little cluttered, so I created folders in the project to group them up.
Once the files are in place you need to open the properties for each one. Change the build action to "Embedded Resource". Now we can access the file streams from code.
image warningImage = new Bitmap(this.GetType().Assembly.GetManifestResourceStream("Application1.images.warning.bmp"));
StreamReader xmlFile = new StreamReader(this.GetType().Assembly.GetManifestResourceStream("Application1.foldername.template.xml"));
GetManifestResourceStream returns a stream. There is a overload for the bitmap constructor that takes a stream if its an image you are working with.
First you have to add the files to the project. I create the images in paint before importing them. The xml files were created within Visual Studio. Things started to look a little cluttered, so I created folders in the project to group them up.
Once the files are in place you need to open the properties for each one. Change the build action to "Embedded Resource". Now we can access the file streams from code.
image warningImage = new Bitmap(this.GetType().Assembly.GetManifestResourceStream("Application1.images.warning.bmp"));
StreamReader xmlFile = new StreamReader(this.GetType().Assembly.GetManifestResourceStream("Application1.foldername.template.xml"));
GetManifestResourceStream returns a stream. There is a overload for the bitmap constructor that takes a stream if its an image you are working with.
Friday, December 14, 2007
NullReferenceException raising an event in C#
I was working with events in C# and ran into this exception. System.NullReferenceException was unhandled Message="Object reference not set to an instance of an object." This exception was at the exact point in my code where I attempted to raise the event. It turns out that if there are not event handlers attached to your event, an attempt to raise it will give you this exception. To solve it, you have to check to see if its null before raising it.
All the samples I was looking at did not check for a null value, so neither did I. Now that I had the problem, I see why I need to check.
public class TestEvent{
public event EventHandler MyEvent;
public void RaiseMyEvent(){
if(MyEvent != null){
MyEvent(this, null);
}
}
}
All the samples I was looking at did not check for a null value, so neither did I. Now that I had the problem, I see why I need to check.
Sunday, December 09, 2007
Running vbs from .net code
There are a few ways to run vbscript inside vb.net. If you just need the script to run, you can use the process object.
Process Class
If you have some VBScript that you want to intereact with variables or run on the fly. Microsoft has a ScriptControl that allows you to do just that. The script control has 3 options of running code.
Dim oProcess as new Process
Try
oProcess.StartInfo.UseShellExecute = True
oProcess.StartInfo.FileName = "CScript c:\script.vbs //nologo"
oProcess.Start()
oProcess.WaitForExit()
Finally
oProcess.Dispose()
End Try
Process Class
If you have some VBScript that you want to intereact with variables or run on the fly. Microsoft has a ScriptControl that allows you to do just that. The script control has 3 options of running code.
- Eval: Evaluates a text expression.
- Run: Runs a named Sub or Function.
- Execute: Executes a script statement.
You can find more information on the ScriptControl at Microsoft.
Q184740: How To Call Functions Using the Script Control
Q184739: INFO: Where to Obtain the Script Control
If you are looking to run vb.net code inside vbscript, see my other post:
Running VB.NET code in VBScript or other apps
Friday, December 07, 2007
Raising an event in C#
Here is the simplest way to raise an event.
This uses the default EventHandler that returns a object source and a EventArgs ex. If you want to pass back different parameters, you will need to create your own delegate. The delegate we just used looks like this:
We dont have to define it because .NET already defined it for us. Here is a second example where we built a custom delegate and raise it.
Now that I explained how to raise events, there is a bug in that code. I'm saving that for my next post: NullReferenceException raising an event in C#
public class TestEvent{
public event EventHandler MyEvent;
public void RaiseMyEvent(){
MyEvent(this, null);
}
}
This uses the default EventHandler that returns a object source and a EventArgs ex. If you want to pass back different parameters, you will need to create your own delegate. The delegate we just used looks like this:
public delegate void EventHandler (object source, EventArgs ex);
We dont have to define it because .NET already defined it for us. Here is a second example where we built a custom delegate and raise it.
public delegate void MyCustomEventHandler (string message);
public class TestEvent2{
public event MyCustomEventHandler MyEvent2;
public void RaiseMyEvent(){
MyEvent2("Message sent with event");
}
}
Now that I explained how to raise events, there is a bug in that code. I'm saving that for my next post: NullReferenceException raising an event in C#