 Wednesday, May 31, 2006
Visual Basic 6's App object exposes a useful property named PrevInstance, which allows you to determine whether there are other instances of the same application running. This property has never been implemented in VB.NET, even though a VB2005 application can use the StartupNextInstance event for this purpose.
' to display this code, open the Application page of the My Project designer and click the Application Events button
Namespace My Partial Friend Class MyApplication Private Sub MyApplication_StartupNextInstance(ByVal sender As Object, ByVal e As Microsoft.VisualBasic.ApplicationServices.StartupNextInstanceEventArgs) Handles Me.StartupNextInstance ' another instance of this application has been launched End Sub End Class End Namespace
The problem of this approach is that the event is raised in the first instance of the application, not in the new instance, therefore when the VB.NET application starts you can't determine for sure whether it is the only instance in the system. In other words, you can learn only whether and when another application is launched, not if the current applicatin is the first and only running instance.
.NET developers have solved this problem in a variety of ways, for example using Mutexes or by iterating over the collection returned by the Process.GetProcesses method. In .NET we have a new and more elegant alternative, based on the System.Threading.Semaphore type. A semaphore is a counter which can be incremented and decremented. If its value becomes zero, a thread can't decrement it and must wait for another thread to increment it to a value >0. Interestingly, if the semaphore has a name it becomes a system-wid object which can be shared by all the applications that ask for a reference to a semaphore with that specific name. It is therefore sufficient that the app creates a semaphore with a unique name just after its launch (e.g. it can be a name that includes the executable's path) to have all the instances of a given app share the same semaphore and therefore the same counter. The only problem left to solve is ensure that the semaphore's counter be correctly resotred when the application terminates, but this is easy to achieve by means of a Finalize method.
In addition to the PrevInstance property - which returns False if the application was the only running instance when the application was launched - the following VB6App class exposes also the InstanceCount property, which returns the total number of instances in that moment (the current app is included in the count). Here's the VB2005 version of this class:
Class VB6App ' the default instance Private Shared DefValue As New VB6App
' the system-wide semaphore Private semaphore As System.Threading.Semaphore ' initial count for the semaphore (very high value) Private Const MAXCOUNT As Integer = 10000
Private Sub New() Dim ownership As Boolean = False ' create a unique name, but strip invalid characters Dim name As String = "VB6App_" & System.Reflection.Assembly.GetExecutingAssembly().Location.Replace(":", "").Replace("\", "") semaphore = New System.Threading.Semaphore(MAXCOUNT, MAXCOUNT, name, ownership) ' decrement its value semaphore.WaitOne() ' if we got ownership, this app has no previous instances m_PrevInstance = Not ownership End Sub
' the PrevInstance property returns True if there was a previous instance running ' when the default instance was created Private Shared m_PrevInstance As Boolean
Public Shared ReadOnly Property PrevInstance() As Boolean Get Return m_PrevInstance End Get End Property
' return the total number of instances of the same application that are currently running Public Shared ReadOnly Property InstanceCount() As Integer Get ' release the semaphore and grab the previous count Dim prevCount As Integer = DefValue.semaphore.Release() ' acquire the semaphore again DefValue.semaphore.WaitOne() ' eval the number of other instances that are currently running Return MAXCOUNT - prevCount End Get End Property
Protected Overrides Sub Finalize() ' increment the semaphore when the application terminates semaphore.Release() End Sub
End Class
Notice that this class has a Finalize method without implementing IDisposable. It is one of those special cases when it is OK to violate the Dispose-Finalize pattern.
To understand how the class works, just keep in mind that the Semaphore.Release method increments the internal counter, whereas the WaitOne method decrements it. You must test the VB6App.PrevInstance property as soon as the application starts, for example in the Sub Main method or in the Load event hander for the main form, as to let the VB6App class store the boolean value internally. The same form might then test the value of the InstanceCount property on exit, for example if you need to run some cleanup code when the last application instance is about to terminate:
Private Sub Form1_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load If Not VB6App.PrevInstance Then ' open the common log file ' ... End If End Sub
Private Sub Form1_FormClosing(ByVal sender As Object, ByVal e As System.Windows.Forms.FormClosingEventArgs) Handles Me.FormClosing If VB6App.InstanceCount = 1 Then ' close the common log file. ' ... End If End Sub
Here's the C# version of the class. (I fixed the original version to fix two syntax errors mentioned in one of the comments below.)
public class VB6App { // the default instance private static VB6App DefValue = new VB6App(); // the system-wide semaphore private System.Threading.Semaphore semaphore; // initial count for the semaphore (very high value) private const int MAXCOUNT = 10000;
private VB6App() { // create a named (system-wide semaphore) bool ownership = false; // create the semaphore or get a reference to an existing semaphore
string name = "VB6App_" + System.Reflection.Assembly.GetExecutingAssembly().Location.Replace(":", "").Replace("\\", ""); semaphore = new System.Threading.Semaphore( MAXCOUNT, MAXCOUNT, name, out ownership); // decrement its value semaphore.WaitOne(); // if we got ownership, this app has no previous instances m_PrevInstance = !ownership; }
// the PrevInstance property returns True if there was a previous instance running // when the default instance was created
private static bool m_PrevInstance ;
public static bool PrevInstance { get { return m_PrevInstance ; } }
// return the total number of instances of the same application that are currently running
public static int InstanceCount { get { // release the semaphore and grab the previous count int prevCount = DefValue.semaphore.Release(); // acquire the semaphore again DefValue.semaphore.WaitOne(); // eval the number of other instances that are currently running return MAXCOUNT - prevCount; } }
~VB6App() { // increment the semaphore when the application terminates semaphore.Release(); } }
 Monday, May 29, 2006
|

|
I have been so busy in May that I couldn't update the blog, not even to mention that my latest Microsoft Press book had been released and is now available on all major US bookstores. With fewer than 600 pages, Programming Microsoft Visual C# 2005: The Base Class Library is surely the shortest book I wrote. Not only that: it's also the book that took me less time to wrote. In fact, this book is basically the translation to C# of the second half of my VB 2005 book, more precisely of the chapters that have to do more with the .NET Framework and less with the C# language itself. Here's the Table of Contents:
1. .NET Framework Basic Types 2. Object Lifetime 3. Interfaces 4. Generics 5. Arrays and Collections 6. Regular expressions 7. Files, directories, and streams 8. Assemblies and resources 9. Reflection 10. Custom attributes 11. Threads 12. Object serialization 13. PInvoke and COM interop |
|
Even though the book isn't specifically on the C# language, it adequately covers most of the new features of C# 2.0, such as generics, iterators, and anonymous methods.
Why a book on the BCL? Well, in these years I realized that far too many developers focus solely on high-level features - such as Windows Forms, ADO.NET, and ASP.NET - and often fail to leverage the full potential from other portions of the .NET Framework. For example, I have seen many apps that use verbose and unefficient validation rules that might be replaced by a single regular expression. Or apps that could be written in a fraction of time (and lines of code) if the author had been conscious of the full potential of reflection and custom attributes. Not to mention the fact that new .NET 2.0 features, such as generics, could make things only worse.
In general I find that most books that are "translated" from a different programming language are disappointing, so you might wonder why this book should be different. First, when I signed the contract for my VB 2005 book I already knew that the book would have been translated to C#, thus I planned the book so that its structure wouldn't be too VB-centric. Secoond, in the last four years I have been using C# in virtually all my programming projects - in fact I have surely written more C# code than VB code. For this reason, you'll find that the C# code is carefully optimized to use all the usual C#-specific techniques, such as iterators and anonymous delegates.
Shortly I will prepare a home page for the book on this site, with a couple of sample chapters. In the meantime, you can read more about the book (and hopefully order it) on Amazon's home page.
 Wednesday, May 03, 2006
Yesterday I got an email from reader Claudio Fontana, with the following, deceiptively simple request: how can you avoid flickering while updating many controls on a form? The problem is especially serious when you need to add thousands of items to a ListView or a TreeView.
In VB6 this problem can be solved quite simply by temporarily setting the Visible (or Enabled) property to False for all the controls about to be updated: the control isn't actually hidden, yet the result of the update operation appears istantaneously when the property is reset to True. Just as interesting: the update operation is carried out much faster if the control is invisible, often twice as faster. Alas, this trick doesn't work in .NET, because as soon as you set the Visible property to False the control is immediately hidden. It's necessary to find another solution.
A few Windows Forms controls - namely the ListBox, ComboBox, ListView, and TreeView controls - do expose the BeginUpdate and EndUpdate methods, which allow you to "freeze" the control while you add items to it. Not only do they solve the flickering problem, they also speed up the update operation, tipically by a factor of 2.5x. However, if your form contains many controls that do NOT expose these methods, you must devise something else, and this was exactly the problem that Claudio submitted, after he unsuccessfully googled around on the 'Net looking for a solution.
The problem is quite intriguing, thus I decided to spend some time on it, until I came to the following solution. The idea is simple, and can be split in the following steps: (1) take a snapshot of the current form's appearance, by making a pixel-by-pixel copy into a bitmap, (2) create a PictureBox control as large as the form, and load the bitmap into the PictureBox, (3) add the PictureBox to the form's Controls collection and bring the PictureBox in front of all other controls, (4) while the user looks at the "frozen" image of the form, update your controls, using the BeginUpdate/EndUpdate mthods if possible to speed up execution, (5) when the update operation is completed, remove the PictureBox from the Controls collection, so that the user can now see the real form.
You just need one dozen statements to implement this algorithim, but I prepared a class to make the code more reusable and to ensure that it releases all resources correctly:
Public Class FormFreezer Implements IDisposable
' The form being frozen Dim Form As Form ' the auxiliary PictureBox that will cover the form Dim PictureBox As PictureBox ' the number of times the Freeze method has been called Dim FreezeCount As Integer = 0
' create an instance associated with a given form ' and optionally freeze the form right away Public Sub New(ByVal form As Form, Optional ByVal freezeIt As Boolean = False) Me.Form = form If freezeIt Then Me.Freeze() End Sub
' freeze the form Public Sub Freeze() ' Remember we have frozen the form once more FreezeCount += 1 ' Do nothing if it was already frozen If FreezeCount > 1 Then Exit Sub
' Create a PictureBox that resizes with its contents PictureBox = New PictureBox() PictureBox.SizeMode = PictureBoxSizeMode.AutoSize ' create a bitmap as large as the form's client area and with same color depth Dim frmGraphics As Graphics = Form.CreateGraphics() Dim rect As Rectangle = Form.ClientRectangle PictureBox.Image = New Bitmap(rect.Width, rect.Height, frmGraphics) frmGraphics.Dispose()
' copy the screen contents, from the form's client area to the hidden bitmap Dim picGraphics As Graphics = Graphics.FromImage(PictureBox.Image) picGraphics.CopyFromScreen(Form.PointToScreen(New Point(rect.Left, rect.Top)), New Point(0, 0), New Size(rect.Width, rect.Height)) picGraphics.Dispose()
' Display the bitmap in the picture box, and show the picture box in front of all other controls Form.Controls.Add(PictureBox) PictureBox.BringToFront() End Sub
' unfreeze the form ' Note: calls to Freeze and Unfreeze must be balanced, unless force=true Public Sub Unfreeze(Optional ByVal force As Boolean = False) ' exit if nothing to unfreeze If FreezeCount = 0 Then Exit Sub ' remember we've unfrozen the form, but exit if it is still frozen FreezeCount -= 1 ' force the unfreeze if so required If force Then FreezeCount = 0 If FreezeCount > 0 Then Exit Sub
' remove the picture box control and clean up Form.Controls.Remove(PictureBox) PictureBox.Dispose() PictureBox = Nothing End Sub
' return true if the form is currently frozen Public ReadOnly Property IsFrozen() As Boolean Get Return FreezeCount > 0 End Get End Property
' ensure that resources are cleaned up correctly Public Overridable Sub Dispose() Implements IDisposable.Dispose Me.Unfreeze(True) End Sub End Class
This is the C# version, translated from VB by Claudio:
public class FormFreezer: IDisposable {
// The form being frozen
Form form;
// the auxiliary PictureBox that will cover the form
PictureBox pictureBox;
// the number of times the Freeze method has been called
int FreezeCount = 0;
// create an instance associated with a given form
// and freeze the form in base of flag freezeIt
public FormFreezer(Form form, bool freezeIt) {
this.form = form;
if (freezeIt) this.Freeze();
}
// freeze the form
public void Freeze() {
// Remember we have frozen the form once more
// Do nothing if it was already frozen
if (++FreezeCount > 1) return;
// Create a PictureBox that resizes with its contents
pictureBox = new PictureBox();
pictureBox.SizeMode = PictureBoxSizeMode.AutoSize;
// create a bitmap as large as the form's client area and with same color depth
Graphics frmGraphics = form.CreateGraphics();
Rectangle rect = form.ClientRectangle;
pictureBox.Image = new Bitmap(rect.Width, rect.Height, frmGraphics);
frmGraphics.Dispose();
// copy the screen contents, from the form's client area to the hidden bitmap
Graphics picGraphics = Graphics.FromImage(pictureBox.Image);
picGraphics.CopyFromScreen(form.PointToScreen(new Point(rect.Left, rect.Top)), new Point(0, 0), new Size(rect.Width, rect.Height));
picGraphics.Dispose();
// Display the bitmap in the picture box, and show the picture box in front of all other controls
form.Controls.Add(pictureBox);
pictureBox.BringToFront();
}
// unfreeze the form
// Note: calls to Freeze and Unfreeze must be balanced, unless force=true
public void Unfreeze(bool force) {
// exit if nothing to unfreeze
if ( FreezeCount == 0 ) return ;
// remember we've unfrozen the form, but exit if it is still frozen
FreezeCount -= 1;
// force the unfreeze if so required
if (force) FreezeCount = 0;
if (FreezeCount > 0) return;
// remove the picture box control and clean up
pictureBox.Controls.Remove(pictureBox);
pictureBox.Dispose();
pictureBox = null;
}
// return true if the form is currently frozen
public bool IsFrozen {
get { return (FreezeCount > 0); }
}
void IDisposable.Dispose()
{
this.Unfreeze(true);
}
}
Using the FormFreezer class is quite simple. Here's a code sample, which assumes that it is located inside a form class so that the Me keyword points to the current form:
Dim ff As New FormFreezer(Me, True) ' update controls here ' ... ff.Unfreeze()
The class implements IDisposable, thus you can bracket the update code in a Using block, either in C# or in VB2005, and avoid an explicit call to Unfreeze:
Using New FormFreezer(Me, True) ' Update controls here ' ... End Using
Notice that calls to Freeze and Unfreeze must be balanced. If you call Freeze twice you then need two calls to Unfreeze to actually restore the updated form. This behavior allows you to call Freeze and then invoke a method that calls Freeze again and still have the code work correctly (provided that all methods use the same instance of the FormFreeze class).
 Monday, May 01, 2006
Many, if not most, Windows Forms samples you can find on the 'net include one or more calls to unmanaged code in Windows DLLs, often in the form of calls to the SendMessage API methods to fix some of the (very few) missing features of .NET controls. The problem is, such a call to unmanaged code creates a problem when the program runs as a ClickOnce application, because it requires higher CAS privileges.
Even though this problem doesn't have a generic solution, when you just need to send a message to the control you are inheriting from, you can avoid an explicit call to SendMessage by invoking the protected DefWndProc method instead. For example, let's say that you are writing an enhanced ComboBox that exposes the TopIndex property, which can set or return the index of the first visible item in the list area. These two operations can be implemented by sending the control the CB_SETTOPINDEX or CB_GETTOPINDEX message, respectively. Here's how you can use the DefWndProc method instead of SendMessage:
Public Class ComboBoxEx Inherits System.Windows.Forms.ComboBox
Public Property TopIndex() As Integer Get Const CB_GETTOPINDEX As Int32 = &H15B Dim m As New Message() m.HWnd = Me.Handle m.Msg = CB_GETTOPINDEX Me.DefWndProc(m) Return m.Result.ToInt32() End Get Set(ByVal value As Integer) Const CB_SETTOPINDEX As Int32 = &H15C Dim m As New Message() m.HWnd = Me.Handle m.Msg = CB_SETTOPINDEX m.WParam = New IntPtr(value) Me.DefWndProc(m) End Set End Property
End Class
By the way, such an enhanced ComboBox can be useful when migrating a VB6 app to VB.NET. In fact, the VB6 ComboBox and ListBox controls expopse the TopIndex property, whereas under .NET only the ListBox control exposes this property. If you have any VB6 code that takes advantage of the ComboBox's TopIndex, the simplest approach is replacing the standard ComboBox with a ComboBoxEx instance.
 Saturday, April 29, 2006
MsgHookX is an ActiveX DLL that allows VB6 developers to perform safe and efficient intra-applicaton subclassing. (In this context, intra-application sublclassing means that you can intercept any message that Windows sends to a window or a control in the current application, as opposed to a window created by another application, a much more difficult task.) I wrote this DLL many years ago, when VB5 was released, and mentioned it in several books and magazine articles. I made the DLL available on the vb2themax site, but I omitted to upload the DLL in this new, .NET-only site.
Well, given the many mails I continue to receive from readers, it seems that VB6 is still alive and in good shape, and many VB6 developers continune to happily subclass their controls. For this reason I decided to make the DLL available again on this site. Here's a summary of what it does:
- safe subclassing: can be used within the IDE and in break mode without any risk of system crashes.
- it provides the BeforeMessage and AfterMessage events for easy event-driven programming model
- it additionally can notify incoming messages through the IMsgHookEvents secondary interface, for better performance and easier debugging (in some cases, events are inhibited in the IDE)
- highest flexibility: you can decide to call the original window procedure yourself from within the BeforeMessage event/method and/or cancel the default processing for the message. You can also browse and modify the value that will be returned to the operating system.
- the DLL's type library includes the definition of over 300 symbolic constants that define the most common Windows messages, so you don't have to use the API Viewer to include them in your applications.
Have fun with it, but - please, please! - turn to VB.NET as soon as you can, if you haven't yet.
I am working at the migration of a large VB6 project and at one point I needed to drop a few methods in the VB6 app, in such a way that the methods would be "invisible" to VB.NET. This action was necessary because these methods performed similarly to a .NET native method. Obviously, I had the option to manually delete the methods after the migration, but if their number is high and if you need to run the wizard more than once on the same VB6 app (as it happens frequently, in the process of preparing the app for the migration), then deleing these portions of code each time becomes a nuisance.
Apparently, you can't instruct the migration wizard to ignore one or more pieces of code. Nevertheless, the solution is quite simple: you just need to bracket these pieces of code - entire methods or just individual statements - in a #If Win32 ...#End If block. The Win32 compilation constant - that is probably ignored by most VB developers today - was introduced when Visual Basic 4 was released, and was recognized also by VB5 and VB6 (but not by any version of VB.NET). Visual Basic 4 is the only version of this language that is available in 16-bit and 32-bit edition, and this compilation constant (together with Win16) allowed to define blocks of code that were compiled under only one of those versions, while allowing to mantain a single source code for both. Therefore, the following VB6 code:
#If WIN32 Then Private Sub Do Something(ByVal n As Integer) ' ... End Sub #End If
is correctly migrated to VB.NET by the migration wizard as follows:
#If WIN32 Then Private Sub DoSomething(ByVal n As Short) ' ... End Sub #End If
but this piece of code will be skipped over by the VB.NET compiler because the Win32 constant isn't defined under VB.NET.
This "feature" can actually create a problem if you are migrating a VB6 app that has been evolved from an older application that was originally written in VB4. In this case, in fact, it is possible that its source code contains one or more #If Win32 blocks. In most cases you *want* to migrate this code, but these portions will be ignored after the migration to VB.NET. If this is the case, you should then locate all the the occurrences of #If Win32 statements in the code and delete them before the migration.
 Monday, March 06, 2006
I am reorganizing my MP3 collection and found that I needed to rename a large quantity of files. Of course, there are many free utilities that allow this operation - and that can use MP3 tags in the process - but I thought that I might write one myself. Thanks to regular expressions, the task shouldn't be that hard. In fact, in a few minutes I came up with the following console application. As you see, most of the code is used to extract and validate arguments on the command line:
Imports System.Text.RegularExpressions Imports System.IO
Module Renx
Function Main(ByVal args() As String) As Integer Console.WriteLine("RENX (C) Francesco Balena / Code Architects Srl")
Dim recurse As Boolean = False Dim renameMode As Boolean = False Dim oldNamePattern As String = Nothing Dim newNamePattern As String = Nothing
' analyze each argument For Each arg As String In args Select Case arg.ToLower() Case "/s", "-s" recurse = True Case "/r", "-r" renameMode = True Case "/h", "-h" Return ShowHelp(0) Case Else If oldNamePattern Is Nothing Then oldNamePattern = "^" & arg & "$" ElseIf newNamePattern Is Nothing Then newNamePattern = arg Else Return ShowHelp(1) End If End Select Next
' check that we have both mandatory arguments If oldNamePattern Is Nothing OrElse newNamePattern Is Nothing Then Return ShowHelp(1) End If ' create the regex and check that pattern syntax is ok Dim reSearch As Regex Try reSearch = New Regex(oldNamePattern, RegexOptions.IgnoreCase) ' test the replace pattern as well Dim tmp As String = reSearch.Replace("a dummy string", newNamePattern) Catch ex As Exception Console.WriteLine("SYNTAX ERROR: {0}", ex.Message) Return 3 End Try Console.WriteLine()
' iterate over all files in current directory (and its subdirectories, if recurse mode) Dim searchOpt As SearchOption = SearchOption.TopDirectoryOnly If recurse Then searchOpt = SearchOption.AllDirectories
Dim parsedFilesCount As Integer = 0 Dim renamedFilesCount As Integer = 0 Dim errorsCount As Integer = 0 For Each oldFile As String In Directory.GetFiles(Directory.GetCurrentDirectory(), "*.*", searchOpt) parsedFilesCount += 1 ' the regex applies to name only Dim oldName As String = Path.GetFileName(oldFile) Dim ma As Match = reSearch.Match(oldName) If ma.Success Then ' this is the new name Dim newName As String = ma.Result(newNamePattern) Console.WriteLine(oldFile) Console.Write(" => {0}", newName) renamedFilesCount += 1 ' proceed with rename only if not in simulation mode If renameMode Then Try Dim dirName As String = Path.GetDirectoryName(oldFile) Dim newFile As String = Path.Combine(dirName, newName) File.Move(oldFile, newFile) Catch ex As Exception Console.Write(" -- ERROR: {0}", ex.Message) errorsCount += 1 End Try End If Console.WriteLine() End If Next
' Display a report If renameMode Then Console.WriteLine("Summary: {0} parsed files, {1} renamed files, {2} errors", parsedFilesCount, renamedFilesCount, errorsCount) Else Console.WriteLine("Summary: {0} parsed files, {1} files affected", parsedFilesCount, renamedFilesCount) Console.WriteLine() Console.WriteLine("NOTE: Running in simulation mode. Specify the /R option to actually rename files.") End If ' Return an error code If errorsCount = 0 Then Return 0 Else Return 2 End If End Function
Function ShowHelp(ByVal exitCode As Integer) As Integer Console.WriteLine() Console.WriteLine("Syntax: RENX <oldnamepattern> <newnamepattern> [/R] [/S] [/H]") Console.WriteLine(" oldnamepattern : regex that selects the files to be renamed") Console.WriteLine(" newnamepattern : regex that specifies how files must be renamed") Console.WriteLine(" /R : rename files") Console.WriteLine(" /S : iterate over subdirectories") Console.WriteLine(" /H : display this help") Console.WriteLine("NOTE: By default the program runs in simulation mode, and just displays how files would be renamed.") Console.WriteLine(" You must specify the /R option to actually rename the files.") Return exitCode End Function
End Module
At the very minimum, the RENX utility requires two arguments: a regex that specifies which files in the current directory (and its subdirectories, if you add the /S option) must be renamed, and a second regex that specifies how to rename the files that are matched by the first regex. The power of RENX is the fact that the first regex can (actually, must) specify one or more groups of characters, and these groups are then referenced in the second regex. For example, let's suppose that I have a folder with the following files:
01 Speak to Me.mp3 02 On the Run.mp3 03 Time.mp3 04 The Great Gig in the Sky.vb3 05 Money.mp3 06 Us and Them.mp3 07 Any Colour You Like.vbr 08 Brain Damage.mp3 09 Eclipse.vb3
and that I want to rename them as follows:
01 - Speak to Me - The Dark Side of the Moon.mp3 02 - On the Run - The Dark Side of the Moon.mp3 03 - Time - The Dark Side of the Moon.mp3 04 - The Great Gig in the Sky - The Dark Side of the Moon.vbr 05 - Money - The Dark Side of the Moon.mp3 06 - Us and Them - The Dark Side of the Moon.mp3 07 - Any Colour You Like - The Dark Side of the Moon.vb3 08 - Brain Damage - The Dark Side of the Moon.mp3 09 - Eclipse - The Dark Side of the Moon.vbr
Here's the RENX command that does it:
RENX "(\d\d) (.+?)(\..+)" "${1} - ${2} - The Dark Side of the Moon.${3}"
Notice that the first regex creates three groups by enclosing them in parenthesis: (\d\d) matches the song number, (.+?) matches the song title, and (\..+) matches the file extension, dot included. The second argument can then reorder these three groups, using the ${N}, where N is the position of the group as specified in the first regex. It is therefore to insert a dash after the song number, and the albumname after the song title.
Because the RENX utility is quite dangerous, by default it doe NOT rename the files, and it just lists how files would be renamed. To actually proceed with the rename operation, you must specify the /R option:
RENX "(\d\d) (.+?)(\..+)" "${1} - ${2} - The Dark Side of the Moon${3}" /R
That's all. You can play with the source code to extend the RENX utility as you prefer, and maybe turn it into a Windows Form application, or you can download the binary version from this link: Renx.zip (5.51 KB)
 Tuesday, February 28, 2006
A few minutes ago I made all the code samples for Programming Microsoft Visual Basic 2005: The Language available on the book home page. It's a 7.5M download that contains several thousand lines of carefully crafted, optimized, reusable source code, including the following:
- a VB class that works with fractions
- a base form for simplified data entry
- a class to cache text files
- several custom iterators for better For Each loops
- tons of examples with generics
- two expression evaluators, the former built on regexes, the latter implemented via on-the-fly compilation
- a utility to display project statistics (a demo for regexes)
- a RegexTester stand-alone utility, to help you build, test and compile regular expressions
- an example of custom provider for My.Settings
- an example of how you can intercept ANY event from ANY set of Windows Forms controls
- attribute-based benchmarks
- a complete infrastructure for writing Windows Forms plug-ins
- an attribute-based library to write data-centric N-tier applications
- several Visual Studio macros
- Visual Studio visualizers for files, regex, and images
- .... and a lot more
On the same page you can also find an errata document for typos and mistakes found after the book went to print.
Enjoy the code (and buy the book if you like it! )
 Thursday, February 23, 2006
The longer I work with generics, the more I like them, and I continue to discover new ways to simplify my code by using them. More specifically, I like the ability to write type-safe code that is also more concise, efficient, and (above all) readable, because I don't have to use tons of CType and DirectCast operators. Today I gathered the generic methods I use more frequently in the following module. They are tiny and simple, yet they save me a lot of time and code.
Module GenericFunctions
' Swap two variables Public Sub Swap(Of T)(ByRef var1 As T, ByRef var2 As T) Dim tmp As T = var1 var1 = var2 var2 = tmp End Sub
' Type-safe version of the IIF function ' returns valueOnTrue if expression is True, else returns valueOnFalse Public Function IIf(Of T)(ByVal expression As Boolean, ByVal valueOnTrue As T, ByVal valueOnFalse As T) As T If expression Then Return valueOnTrue Else Return valueOnFalse End If End < |