Originally published December 2002 on DoItIn.net using VB7.0/2002. Updated for VB7.1 February 2005
Links for compiled demo versions, all required resources and source code are included at the end of this article.
Plus, get the complete eBook in Adobe Acrobat 7 format ... all here.
17) Final Cleanup
The big chunk of the work is done, now we just need to patch up those little things that make the difference between "I can use my control" and "Anyone can use my control"
Clear the control
Let's add a method that initializes the control; clearing the picturebox, setting the picturebox square to fill the visible area, forcing the page count variable to zero and resetting the imagepage array so all of those "m_ImagePages Is Nothing" lines actually do something.
This routine should be called by the control's Load event and we'll make the sub that does the work Public so the dev-user can easily re-initialize the widget on-demand.
Public Sub InitializeControl()
m_CurrentPageNumber = 0
''init the array
m_ImagePages = Nothing
pbDisplay.Image = Nothing
pbDisplay.Size = Me.Size
End Sub
Now add the call to the control's Load event:
Private Sub svImageEditor_Load(ByVal sender As Object, _
ByVal e As System.EventArgs) Handles MyBase.Load
InitColorDepth()
InitializeControl()
End Sub
When the dev-user first adds an instance of the control to a form it will appear at the same size you see it in development and the picturebox won't be re-sized by the InitializeControl method. You can tweak UC's GUI designer to a specific size, and you should use the property sheet to explicitly make the picturebox size the same as the UC's size (setting the picturebox BorderStyle to None is up to you).
When the dev-user is resizing the control in their IDE or the user is resizing their form with the control fully anchored the above InitializeControl code won't keep the picturebox positioned correctly at the edges of the control. Patch this issue with code in the UC's private OnResize event. Notice that the event you want to use is the protected OnResize from the "(Overrides)" event list, not the control's own "Resize" event, if you use the standard Resize then the picturebox won't keep up with the UC and you'll get flashing and intermittent scrollbars during resizes.
Protected Overrides Sub OnResize(ByVal e As System.EventArgs)
If m_ImagePages Is Nothing Then
pbDisplay.Size = Me.Size
Else
'this is bit of a hack
'an anchored usercontrol gets the event
'but the autoscroll bars don't respond
'to it if the internal control (pbDisplay)
'isn't also resized
'by "reminding" the UC of the setting
'the bars get the repaint requests
Me.AutoScroll = True
End If
End Sub
Tip: You should be fine with the above but if even with the above you get scrollbar flashing on your typical user machines, you can try using a static variable in the OnResize event as we always had to do in VBClassic. For an example of that technique, click here.
As you've been doing your unit tests, you've probably just ignored all of the greyed out custom properties, the ones that are either readonly or not applicable at designtime (CurrentDisplayImage, CurrentPageNumber, TotalPageCount). Another dev-user down the line might find those positively distracting and they're easy to "hide" with the System.ComponentModel.Browseable attribute. Just add the attribute to each of the properties as shown here with CurrentDisplayImage:
<System.ComponentModel.Browseable(False)> _
Public ReadOnly Property CurrentDisplayImage() As Image
Get
Return DirectCast(m_ImagePages(m_CurrentPageNumber).WorkingImage, Image)
End Get
End Property
The properties will still pop up in designtime intellisense and they'll be fully available to runtime code, all we're doing here is removing them from the Visual Studio property sheet grid.
In a similar vein, there are some usercontrol base properties that don't make sense in the property sheet and also you don't want to have mucked with in code. You can't hide them from code, but you can short circuit them so even if the dev-user does try to use them they won't hurt the control.
A good example of such a property is "AutoScroll". Our control depends on this property of the usercontrol always being set to True and so we don't want it showing up in the property sheet AND we want to ignore any external requests to set it to False. The trick is to use the Browseable attribute on a public property of the same name declared as "Shadows". Using Shadows lets you trap the base class's properties and methods and process them yourself (like hooking windows messages in VBClassic). There are a bunch that get in the way in my opinion, so I created a new sub-region in "Public Properties" to keep them all "hidden" together:
#Region "Public Properties"
#Region "Hidden Base Properties"
'hide this from the property sheet
<System.ComponentModel.Browseable(False)> _
Public Shadows Property AutoScroll() As Boolean
Get
Return MyBase.AutoScroll
End Get
Set(ByVal Value As Boolean)
'we need to use this internally
'external attempts should be ignored
'so do nothing
End Set
End Property
'hide this from the property sheet
<System.ComponentModel.Browseable(False)> _
Public Shadows Property AutoScrollMargin() As Size
Get
Return MyBase.AutoScrollMargin
End Get
Set(ByVal Value As Size)
'we need to use this internally
'external attempts should be ignored
'so do nothing
End Set
End Property
'hide this from the property sheet
<System.ComponentModel.Browseable(False)> _
Public Shadows Property AutoScrollMinSize() As Size
Get
Return MyBase.AutoScrollMinSize
End Get
Set(ByVal Value As Size)
'we need to use this internally
'external attempts should be ignored
'so do nothing
End Set
End Property
'hide this from the property sheet
<System.ComponentModel.Browseable(False)> _
Public Shadows Property BackgroundImage() As Image
Get
Return MyBase.BackgroundImage
End Get
Set(ByVal Value As Image)
'we need to use this internally
'external attempts should be ignored
'so do nothing
End Set
End Property
'devs are used to this not doing anything
'why not just dump it since it makes no sense
'hide this from the property sheet
<System.ComponentModel.Browseable(False)> _
Public Shadows Property RightToLeft() As RightToLeft
Get
Return RightToLeft.No
End Get
Set(ByVal Value As RightToLeft)
'we need to use this internally
'external attempts should be ignored
'so do nothing
End Set
End Property
'like a picturebox, we don't want a tabstop
'hide this from the property sheet
<System.ComponentModel.Browseable(False)> _
Public Shadows Property TabStop() As Boolean
Get
Return False
End Get
Set(ByVal Value As Boolean)
'we need to use this internally
'external attempts should be ignored
'so do nothing
MyBase.TabStop = False
End Set
End Property
#End Region
....
Kinda funny huh? It's just this kind of silent property "eating" that we said we hated about Visual Studio's control size properties ... ah well, at least ours won't crash the framework if they're used ;-).
Some folks say to also use the EditorBrowseable() attribute if you want to hide base properties from the Visual Studio code editor. I say stick to just Browseable() and protecting the input, EditorBrowseable() is only handled in the VB.Net code editor (C# ignores it) and even under VB I've found it doesn't always work.
One more thing on the look of the control in dev-user mode: we needed the yellow for visibility during development but now we should change the UC's backcolor to make it look a bit more professional when it first gets dropped on a form; Change it to System>Control with the IDE property sheet.
We did a lot of work to protect the control from hitting an unhandleable exception during Zooms but our lowballed limit may be far below what is acceptable to Windows at different dimension ratios. Until someone figures out an algorithm to quickly test the actual graphics limits, our lowballing will keep zooms from crashing programs and we need similar protection in the LoadImage method.
The easy way out is to test the Image dimensions and if either hit our limit then raise an exception and don't do the load. That works, but there's another almost-as-easy way: If the image object can be created then resize it to fit the maximum allowable dimensions. The good news is that we've already laid the foundation for this route in the LoadImage's use of CalcZoomPercentLimits. All we need to do is call that method before we set the svImagePage's CurrentZoom member, test the source image sizes against the limits and if they're gong to be exceeded then set CurrentZoom to the MaximumZoomPercent; at the same time we can protect tiny images by checking against the MIN_ZOOM_PIXELS constant. Because our ResetImage will also be initializing zoom percentages we'll need to do the calculation there too, so it's best to create a new private sub that both methods can use:
Private Sub InitImagePercent(ByRef imagePage As svImagePage)
If imagePage.WorkingImage.Width >= m_MaxControlDimension OrElse _
imagePage.WorkingImage.Height >= m_MaxControlDimension Then
imagePage.CurrentZoom = imagePage.MaxZoomPercent
ElseIf imagePage.WorkingImage.Width <= MIN_ZOOM_PIXELS OrElse _
imagePage.WorkingImage.Height <= MIN_ZOOM_PIXELS Then
imagePage.CurrentZoom = imagePage.MinZoomPercent
Else
imagePage.CurrentZoom = 100
End If
End Sub
Now you can update the LoadImage (and replace its call to ResetImage with a call to UpdateDisplay, ResetImage was redundant in this situation anyway)
Public Function LoadImage(ByVal img As Image) As Boolean
Dim pageCount As Integer
Dim tmpFileImage As Image
Dim tmpFrameImage As Image
Try
m_CurrentPageNumber = 0
tmpFileImage = DirectCast(img.Clone, Image)
pageCount = SourceFilePageCount(tmpFileImage)
''reset the array to the number of pages
ReDim m_ImagePages(pageCount - 1)
For i As Integer = 0 To UBound(m_ImagePages)
tmpFileImage.SelectActiveFrame(FrameDimension.Page, i)
tmpFrameImage = DirectCast(tmpFileImage.Clone, Image)
tmpFrameImage = CopyImage(tmpFrameImage)
m_ImagePages(i).OriginalImage = tmpFrameImage
m_ImagePages(i).WorkingImage = DirectCast(tmpFrameImage.Clone, Image)
CalcZoomPercentLimits(m_ImagePages(i))
InitImagePercent(m_ImagePages(i))
Next
UpdateZoomDisplay()
RaiseEvent ImageLoaded(Me, New svImageLoadEventArgs(pageCount))
Catch ex As Exception
'todo: test to figure errors
' and remove catch-all
Throw ex
Finally
If Not tmpFrameImage Is Nothing Then
tmpFrameImage = Nothing
End If
If Not tmpFileImage Is Nothing Then
tmpFileImage = Nothing
End If
End Try
End Function
Public Sub ResetImage(Optional ByVal resetRotation As Boolean = True, _
Optional ByVal resetZoom As Boolean = True, _
Optional ByVal resetAll As Boolean = False)
If Not m_ImagePages Is Nothing AndAlso m_ImagePages.Length > 0 Then
If resetAll Then
For i As Integer = 0 To UBound(m_ImagePages)
If resetRotation Then
m_ImagePages(i).WorkingImage = _
DirectCast(m_ImagePages(i).OriginalImage.Clone, Image)
End If
If resetZoom Then
InitImagePercent(m_ImagePages(i))
End If
Next
Else
If resetRotation Then
m_ImagePages(m_CurrentPageNumber).WorkingImage = _
DirectCast(m_ImagePages(m_CurrentPageNumber).OriginalImage.Clone, Image)
End If
If resetZoom Then
InitImagePercent(m_ImagePages(m_CurrentPageNumber))
End If
End If
UpdateZoomDisplay()
End If
End Sub
Use your favorite drawing program to create a 32767(w)x2000(h) image and give it a test. On my machine this will set the max and the initial display to 40%. Trying it again with 2000(w)x32767(h), however, generates a trappable exception in the Image.FromImage call of the file version of LoadImage:

Oy veh. It's not the big fatal kahuna but "Generic Exception occurred in GDI+" isn't a great message to pop up to the user. We'll lay a trap for this in the filepath version of LoadImage, and convert it to an svInvalidImage exception, using a slightly different message.
Here's hoping we eventually get the time to go deep into GDI+ to figure out the image by image limits on-demand. Until that time realistic limits and usable exception information will get projects to the users and the majority of users will be more than satisfied (Over the years, did you ever hit that control size limit bug in your apps? It's been there from the beginning but you had to hit an unrealistic size to find it).
Public Function LoadImage(ByVal fileName As String) As Boolean
If File.Exists(fileName) Then
Try
Return LoadImage(Image.FromFile(fileName))
Catch ex As OutOfMemoryException
Throw New svImageInvalidImageFileException(ex)
Catch ex As System.Runtime.InteropServices.ExternalException
Throw New svImageInvalidImageFileException( _
"Source image could not be loaded." & vbCrLf & _
"Width or height may exceed allowable limits.", _
ex)
Catch ex As Exception
'todo: test to figure errors
' and remove catch-all
Throw ex
Finally
'even though we are technically making a copy
'.Net will lock the source file while the app is
'is running
'the way around this is another necessary evil
GC.Collect
End Try
Else
Throw New FileNotFoundException
End If
End Function
We've also added a GC.Collect in LoadImage's Finally. If you don't do this and your dev-user tries to allow overwriting of a loaded image file they'll get a "Generic GDI+ Exception". While many devs frown on ever forcing the garbage collector to run, there are times when it is the right thing to do and releasing the lock on a loaded image file is definitely one of them. Even though we're creating clones of the file's images and not working directly with the file, .Net isn't smart enough to release the unmanaged resource till our app is closed (.Net2's garbage collector supposedly will release unmanaged resources more effectively).
And on the subject of memory...
Here's one last that will make a huge difference to your users: Implement the hook for the control's Disposed event and use it to pass Dispose calls to the image objects in memory. My sincerest thanks goes to Riaan Greyling of South Africa for emailing me about the absolute need for this. Riaan added the hook in a new region of the UC and I'll quote you his code exactly:
#Region " General Component Events "
Private Sub svImageEditor_Disposed(ByVal sender As Object, ByVal e As System.EventArgs) _
Handles MyBase.Disposed
Dim ThisLoop As Integer
For ThisLoop = 0 To m_ImagePages.Length - 1
m_ImagePages(ThisLoop).WorkingImage.Dispose()
m_ImagePages(ThisLoop).OriginalImage.Dispose()
Next
End Sub
#End Region
As mentioned, while the CLR garbage collector should eventually kick in and reclaim even unmanaged resources (like memory used for images), before that happens your program could slow to a crawl and really upset your users. Implementing this hook will instantly give your control's apps a visible snappiness.
The control fits its part of the application spec and all that's left for the dev-user is to hook up some menus and buttons and relatively simple image saving code to the ImageCropped event. They probably won't want to make a file right at that event so to save them having to hold their own image variable we'll add a quick public property that let's them get at the currently displayed image at any time.
Public ReadOnly Property CurrentDisplayImage() As Image
Get
Return DirectCast(m_ImagePages(m_CurrentPageNumber).WorkingImage, Image)
End Get
End Property
Alternatively you could use a clone of pbDisplay's Image, but if you do remember to check for Nothing and return Nothing explicitly if pbDisplay has no image. If you don't then the dev-user will get a NullReferenceException (what devs from VBClassic will always call "A 91").
At one presentation, a person asked "why use a method for loading and a readonly property for reading instead of a R/W Image property?" The answer, as always in this project is: "MultiPage TIffs". I'm of the opinion that it would be confusing to the dev-user to have a single "Image" property that lets you pass in a four page file and only passes out a single page, and the flipside option of passing out an array of all of the WorkingImages would be even more confusing since now they'd need to check the CurrentPageNumber property to work with the image they care about. With the method and RO property what they're doing and what they're getting are obvious.
We'd just mentioned that it's up to the dev-user to save off the image objects, the control's purpose is to display, rotate and crop and keeping image file format code out of it means it's less likely to need maintenance as file format desires change. We just export an "Image" type and the rest is almost script code.
That being said, we can provide some optional functionality that to export a resized Image object so they don't have to do their own DrawImage code for thumbnailing or other resize requirements. For this, we'll use two slightly modified semi-intelligent versions of the generic Smithvoice.com Aspect-correct resize routine. One that returns an aspect correct new image and one that returns an image set to a specific size.
Both of these new routines are essentially copying images so we could overload the CopyImage function, but in my mind the aspect correct version would be more understandable with a unique name.
Open the ImageUtilities.vb file and add the new functions:
Friend Function AspectResizeImage(ByVal sourceImage As Image, _
ByVal maxSize As Integer, _
Optional ByVal ByWidth As Boolean = True, _
Optional ByVal interpolation As InterpolationMode = InterpolationMode.HighQualityBicubic) _
As Image
Dim ratio As Single = 1
Dim newWidth, newHeight As Integer
Dim bmpSource As Bitmap
Dim bmpDest As Bitmap
Dim gfx As Graphics
Try
bmpSource = New Bitmap(sourceImage)
ratio = CSng(bmpSource.Width / bmpSource.Height)
If ByWidth Then
newWidth = maxSize
newHeight = CInt(maxSize / ratio)
Else 'by height
newWidth = CInt(maxSize * ratio)
newHeight = maxSize
End If
'make bitmap for result
bmpDest = New Bitmap(newWidth, newHeight)
'create Graphics object for final bitmap
gfx = Graphics.FromImage(bmpDest)
gfx.InterpolationMode = interpolation
'blast the source image into the dest
gfx.DrawImage(bmpSource, 0, 0, bmpDest.Width, bmpDest.Height)
Return DirectCast(bmpDest.Clone, Image)
Catch ex As OutOfMemoryException
Throw ex
Catch ex As ArgumentOutOfRangeException
Throw ex
Catch ex As Exception
'ToDo: remove after complete testing
Throw ex
Finally
If Not (sourceImage Is Nothing) Then
sourceImage.Dispose() ' get rid of the old one if it exists.
End If
If Not (bmpSource Is Nothing) Then
bmpSource.Dispose() ' get rid of the old one if it exists.
End If
If Not (bmpDest Is Nothing) Then
bmpDest.Dispose() ' get rid of the old one if it exists.
End If
If Not gfx Is Nothing Then
gfx.Dispose()
End If
End Try
End Function
Friend Function CopyImage(ByVal sourceImage As Image, _
ByVal finalwidth As Integer, ByVal finalHeight As Integer, _
Optional ByVal interpolation As svInterpolationMode = _
svInterpolationMode.HighQualityBicubic) As Image
Dim bmpSource As Bitmap
Dim bmpDest As Bitmap
Dim gfx As Graphics
Try
bmpSource = New Bitmap(sourceImage)
''make bitmap for result
bmpDest = New Bitmap(finalwidth, finalHeight)
'create Graphics object for final bitmap
gfx = Graphics.FromImage(bmpDest)
gfx.InterpolationMode = CType(interpolation, InterpolationMode)
'blast the source image into the dest
gfx.DrawImage(sourceImage, 0, 0, bmpDest.Width, bmpDest.Height)
Return DirectCast(bmpDest.Clone, Image)
Catch ex As OutOfMemoryException
Throw ex
Catch ex As Exception
Throw ex
'Todo: remove after complete testing
Finally
If Not (bmpSource Is Nothing) Then
bmpSource.Dispose() ' get rid of the old one if it exists.
End If
If Not (bmpDest Is Nothing) Then
bmpDest.Dispose() ' get rid of the old one if it exists.
End If
If Not gfx Is Nothing Then
gfx.Dispose()
End If
End Try
End Function
Unlike the generic smithvoice.com version, these have no size protection code and they're declared as Friend so making them only accessible inside the UC's assembly. The user will get at them via wrapper functions in the public Methods region of the UC. Notice that we're not limiting the minimum size to MIN_ZOOM_PIXELS since that is a limit we defined only for our display, instead we've hard coded a lower limit of 1, that may not be a very useful size but many web pages use 1x1 images so it's at least got precedent.
Public Function ResizedDisplayImageAspectCorrect( _
ByVal maxSize As Integer, _
Optional ByVal byWidth As Boolean = True, _
Optional ByVal interpolation As svInterpolationMode = _
svInterpolationMode.HighQualityBilinear) _
As Image
Dim tmpPageImage As svImagePage
If Not interpolation.IsDefined(GetType(svInterpolationMode), interpolation) Then
Throw New ArgumentOutOfRangeException("interpolation", _
"Invalid svInterpolationMode")
Else
If Not m_ImagePages Is Nothing AndAlso m_ImagePages.Length > 0 Then
tmpPageImage = m_ImagePages(m_CurrentPageNumber)
If (maxSize >= tmpPageImage.WorkingImage.Width * tmpPageImage.MaxZoomPercent) OrElse _
(maxSize >= tmpPageImage.WorkingImage.Height * tmpPageImage.MaxZoomPercent) OrElse _
maxSize < 1 Then
Throw New ArgumentOutOfRangeException("maxSize", _
maxSize, _
"Supported resize widths are between 1 and " _
& m_MaxControlDimension.ToString & " pixels")
Else
Return AspectResizeImage(tmpPageImage.WorkingImage, maxSize, byWidth, interpolation)
End If
End If
End If
End Function
Public Function ResizedDisplayImageSpecific( _
ByVal finalWidth As Integer, _
ByVal finalHeight As Integer, _
Optional ByVal interpolation As svInterpolationMode = _
svInterpolationMode.HighQualityBilinear) _
As Image
Dim tmpPageImage As svImagePage
If Not interpolation.IsDefined(GetType(svInterpolationMode), interpolation) Then
Throw New ArgumentOutOfRangeException("interpolation", _
"Invalid svInterpolationMode")
Else
If Not m_ImagePages Is Nothing AndAlso m_ImagePages.Length > 0 Then
tmpPageImage = m_ImagePages(m_CurrentPageNumber)
If (finalWidth >= tmpPageImage.WorkingImage.Width * tmpPageImage.MaxZoomPercent) OrElse _
finalWidth < 1 Then
Throw New ArgumentOutOfRangeException("finalWidth", finalWidth, _
"Supported resize widths are between 1 and " _
& m_MaxControlDimension.ToString & " pixels")
ElseIf (finalHeight >= tmpPageImage.WorkingImage.Height * tmpPageImage.MaxZoomPercent) OrElse _
finalHeight < 1 Then
Throw New ArgumentOutOfRangeException("finalHeight", finalHeight, _
"Supported resize heights are between 1 and " _
& m_MaxControlDimension.ToString & " pixels")
Else
Return CopyImage(tmpPageImage.WorkingImage, finalWidth, finalHeight, interpolation)
End If
End If
End If
End Function
In these wrappers we're doing the size checking and ... darn it, you're right, tossing exceptions may do the trick but it'd be better if we told the dev up front what the limits are so they wouldn't have to apply and hope. And exposing the interpolation mode is risky too because the enumeration includes a silly value named "Invalid" that will crash DrawImage if applied (why the heck is that in the enum?)
Ok, back to front, we need a wrapper around the InterpolationMode enum just to get rid of the "Invalid". Put it in the svTypes.vb file. Note the required brackets around the VB keyword "Default", and that I made it with bytes because I really hate the -1 Invalid member that much ;-).
Public Enum svInterpolationMode As Byte
[Default] = 0
Low = 1
High = 2
Bilinear = 3
Bicubic = 4
NearestNeighbor = 5
HighQualityBilinear = 6
HighQualityBicubic = 7
End Enum
Now in svTypes.vb define a class to hold the properties. Although it is going to end up very similar to the svImagePage structure we're doing it as a class this time because we're exposing it to the dev-user and OPP is the correct way to do that in .Net and also a class lets us make all the properties readonly and the constructors "hidden" by sign Friend declarations:
Public Class svDisplayImageDetail
Private m_PageNumber As Integer = 0
Private m_PagesInFile As Integer = 0
Private m_Width As Integer = 0
Private m_Height As Integer = 0
Private m_CurrentZoom As Integer = 0
Private m_MaxZoom As Integer = 0
Friend Sub New()
End Sub
Friend Sub New(ByVal pageNumber As Integer, ByVal pagesInFile As Integer, _
ByVal width As Integer, ByVal height As Integer, _
ByVal currentZoom As Integer, ByVal maxZoom As Integer)
m_PageNumber = pageNumber
m_PagesInFile = pagesInFile
m_Width = width
m_Height = height
m_CurrentZoom = currentZoom
m_MaxZoom = maxZoom
End Sub
Public ReadOnly Property PageNumber() As Integer
Get
Return m_PageNumber
End Get
End Property
Public ReadOnly Property PagesInFile() As Integer
Get
Return m_PagesInFile
End Get
End Property
Public ReadOnly Property Width() As Integer
Get
Return m_Width
End Get
End Property
Public ReadOnly Property Height() As Integer
Get
Return m_Height
End Get
End Property
Public ReadOnly Property CurrentZoom() As Integer
Get
Return m_CurrentZoom
End Get
End Property
Public ReadOnly Property MaxZoom() As Integer
Get
Return m_MaxZoom
End Get
End Property
Public ReadOnly Property MaxHeight() As Integer
Get
Return m_Width * m_MaxZoom
End Get
End Property
Public ReadOnly Property MaxWidth() As Integer
Get
Return m_Height * m_MaxZoom
End Get
End Property
End Class
Now just add the readonly public property to the UC (hiding it from the property sheet since it makes no sense at designtime and will only show a null reference then anyway):
<System.ComponentModel.Browseable(False)> _
Public ReadOnly Property CurrentImageDetail() As svDisplayImageDetail
Get
Dim oID As svDisplayImageDetail
Dim tmpImagePage As svImagePage
If Not m_ImagePages Is Nothing AndAlso m_ImagePages.Length > 0 Then
tmpImagePage = m_ImagePages(m_CurrentPageNumber)
oID = New svDisplayImageDetail( _
m_CurrentPageNumber + 1, _
m_ImagePages.Length, _
tmpImagePage.WorkingImage.Width, _
tmpImagePage.WorkingImage.Height, _
tmpImagePage.CurrentZoom, _
tmpImagePage.MaxZoomPercent)
Return oID
Else
oID = New svDisplayImageDetail()
End If
Return oID
End Get
End Property
Give it a fast test in testHarness with this kind of code in a button click:
Private Sub butShowDetails_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles butShowDetails.Click
Dim oID As svDisplayImageDetail = SvImageEditor1.CurrentImageDetail
MsgBox("Page: " & oID.PageNumber.ToString & " of " & oID.PagesInFile.ToString & vbCrLf & _
"width: " & oID.Width.ToString & vbCrLf & _
"height: " & oID.Height.ToString & vbCrLf & _
"max width: " & oID.MaxWidth.ToString & vbCrLf & _
"max height: " & oID.MaxHeight.ToString & vbCrLf & _
"cur zoom: " & oID.CurrentZoom.ToString & vbCrLf & _
"max zoom: " & oID.MaxZoom.ToString)
End Sub
That's more useful.
|
This class's object is being created on every call to the CurrentImageDetail property, and on each call it's just tossing up a lot of values copied over from the page structure. I don't think any end user is going to notice the performance hit but it's not very efficient.
Welcome to Experience.
Way back in the beginning we decided to be "efficient" by internally using an array of structures for the page information instead of a collection of objects and now it seems that had we done OOP internally it would been more efficient in the long run because we could be exposing a readonly interface of a page object for the CurrentImageDetail.
Going back and changing this fundamental building block now would be a lot of work for little real benefit so we're not going to do it, but keep the scenario in the back of your mind because the vast majority of applications have internal collections and there will be similar ramifications of the code styles you choose.
|
Like the yellow background, the FixedSingle border around the picturebox was helpful for design but might not be wanted in an app. We could expose the full borderstyle enumeration but the inset Fixed3D style is even more useless in this context so instead we'll make a boolean property to turn the FixedSingle display on and off:
Public Property ShowImageBorder() As Boolean
Get
Return pbDisplay.BorderStyle = BorderStyle.FixedSingle
End Get
Set(ByVal Value As Boolean)
Select Case Value
Case True
pbDisplay.BorderStyle = BorderStyle.FixedSingle
Case Else
pbDisplay.BorderStyle = BorderStyle.None
End Select
End Set
End Property
I won't bother creating a special icon for the control. IDE Toolbox icons are truly important for shareware and retail releases - which my exact code given freely here for educational purposes is NOT allowed to be used for - but for corporate releases it's up to you. If you want to add an icon you can find lots of tips on the web.
Once you've got the code set and you're happy with the unit tests and you've gone through and removed all of the "ToDo" comment lines you can pop it in an app ... AFTER you've got the version system set.
Similar to VB6, VB.Net allows you to switch on and off an autoincrement for your version numbers but for some reason C# got the full old VB6 functionality and VB.Net got some pain.
To set your versions, you have to edit the text in the AssemblyInfo.vb file. Open that file and look down near the bottom, this is the "autoincrement" area:
' You can specify all the values or you can default the Build and Revision Numbers
' by using the '*' as shown below:
<Assembly: AssemblyVersion("1.0.*")>
From that you'd assume that every time you build you'd get an updated Revision and Build value but that's the case only in C#. VB.Net doesn't automatically increment the assembly versions when you use wildcards in the version attribute of the assemblyinfo file if you're running the same instance of the IDE (unless you switch from a non-strongnamed build to a strongnamed build) and VB developers are urged to set version numbers explicitly, whereas rebuilding with C# will give a new version number on every build in the same IDE instance. This isn't a bug - it's not well documented, but it's not a bug.
At first that pissed me off, how dare MS give C# the decade-old "VB way" and make VBers have to jump through hoops. It seemed to me that Microsoft was thinking that VB people were going to do more rebuilds than C# people and so they made the IDE ignore our updates. But the more I pondered it the more I found that VB got the good end of the stick. In VBClassic we did mostly need to shut off autoincrement while we were working out code and only turn it on again when we thought we were ready for releasing to testers - if we didn't do this then testers got upset that we'd go from version XX.01 to XX.27. On code complete some people sometimes forgot to turn the incrementer back on and ended up giving the testers a new build with an old version number - not that I ever did that of course ;-). With the new IDE, all you have to do is build build build and THEN, just before you release to your test team you manually increment. I think it's easier to remember to manually update the version number than it was to remember to turn off the autoincrementer.
So, we're happy with the code and we're going to manually set the version number:
' You can specify all the values or you can default the Build and Revision Numbers
' by using the '*' as shown below:
<Assembly: AssemblyVersion("1.0.0.0")>
That alone might be good enough for some quickie XCOPY apps where the company or user isn't concerned about security and where the control dll is always going to be in the same folder as the exe. Remember though, that even manual versioning is only helping humans and only helping to a point, when you're making components you should use manual versioning AND Strong Naming.
Consider your new control compiled without strong naming. A dev-user will grab the compiled dll and pop it in their WinForm and hook it all up to their GUI and everything is great. At the end they follow their corporate security mandate and add a strong name to their exe (because corporate machines are rightly set to only trust exes from known sources and the company's key is given high trust) and they hit the build button and they get an error. They can't build a strong named exe unless all of the components it uses are strong named. It really isn't a security thing (just because something has a key file doesn't mean it's safe) it's just the way it is, so component dlls, controls or code libraries, should be strong named or they'll be worthless to devs who have to make strong named apps.
Version and strong name key file noted in the AssemblyInfo.vb file, set the build type to "Release" and build your svImageEdit control project. Put all of the solution files into your source control system (If you're not running Perforce or SourceSafe put it all in a zip and save it off somewhere you won't lose it). Close the IDE and start a new instance, it's time to wrap the app.
Next: Making the application
Robert Smith
Kirkland, WA
added to smithvoice march 2005