Thursday, October 23, 2008

LIKE is not case insensitive?

I was stuck by understanding the following SQL query:


SELECT name
FROM my_table
WHERE name LIKE '%Jack%'

There is nothing special. It just tries to pick up all "names" of "my_table", those values contain the word "Jack" (case sensitive). However, "jack" is also in the result. I cannot believe my eyes.

I googled "LIKE case insensitive" and got the answer:
The problem is caused by the collation of the database. The default collation of my database is "latin1_swedish_ci". The trailing "_ci" stands for case insensitive. I changed it to "utf8_bin", the problem has been solved.

Here are some useful links:
http://forums.mysql.com/read.php?20,202839,202839
http://dev.mysql.com/doc/refman/5.0/en/pattern-matching.html
http://dev.mysql.com/doc/refman/5.0/en/charset-charsets.html

Wednesday, September 10, 2008

require_once()

If you used require_once(), you may have known one big issue of it: "If you give a relative path to the file you want to require, PHP will use the working directory as root, not the directory where this file is located." Your code won't have any problem, except the file to be required, requires some other files as well and unfortunately they are not in a same directory.

There are several solutions on the Internet.

  1. Put all PHP files into one folder. This works for tiny projects, but a flat folder structure is really ugly.
  2. Use a PHP framework, like Zend, CodeIgniter, CakePHP. They provide a certain function like $this->load() to find the correct path of these files and load them.
  3. If your code is not based on a framework, you can simply use
    "require_once(dirname(__FILE__).'/path_to_file')" instead of
    "require_once('path_to_file')". However this does not work on my xampp environment. My code hangs up when executing this line. (The problem seems not to be on the slash or backslash. Really no idea)

My Solution

Write a function to calculate the relative path from file directory to working directory. If you previously use "require_once('path_to_file')", now you write
"require_once(cp('path_to_file'))". It has been tested on Windows.

if (!function_exists('cp'))
{
function cp($f) // change_path
{
if (($wp = getcwd()) === ($fp = dirname(__FILE__))) { return $f; }
$wp = explode('\\', $wp);
$fp = array_merge(explode('\\', $fp), explode('/', $f));
for ($i = 0, $p = min(count($wp), count($fp)); $i < $p; $i++)
{
if ($wp[$i] === $fp[$i])
{
array_shift($wp);
array_shift($fp);
$i--; $p--;
continue;
}
break;
}
$p = ''; foreach ($wp as $k) { $p .= '../'; }
return $p.implode('/', $fp);
}
}

Saturday, August 9, 2008

Quick note on UnicodeString

Jan Goyvaerts has introduced some key information about Unicode in Delphi 2009.
http://www.micro-isv.asia/2008/08/get-ready-for-delphi-2009-and-unicode/

Marco Cantu has made a performance comparison between AnsiString and UnicodeString.
http://blog.marcocantu.com/blog/tiburon_unicode_video_5.html

Conclusion

According to the information, so much I have collected in the past a few months, I summarized them as the following points:
  1. Simple projects can be upgraded to Delphi 2009 without any modifications. Projects with heavy string operations or file operations could be compiled as well. But they should be tested completely before release.
  2. UnicodeString is based on the same concept as AnsiString. So it is very fast and efficient, although it is a little bit slower than AnsiString.
  3. In compare with WideString much less memory will be allocated for UnicodeString. If it is not necessary, you had better switch WideStrings to UnicodeStrings for a performance boost.

UPDATE 1: Here and here are detailed performance tests
 

Tuesday, June 3, 2008

Google features in a shell

Just found an interesting site http://goosh.org/. It is powered by Google APIs.

 

Tuesday, May 27, 2008

Do not give the OK button a keyboard accelerator

Raymond Chen, a well-known developer on the Windows Shell team at Microsoft, mentioned a principle about dialog design in his blog: Do not give the OK button a keyboard accelerator [1].

But it is funny, that even their own products do not follow this principle strictly. A remarkable example is the "Restart Prompt Dialog of Automatic Updates" in WindowsXP. Both buttons "Restart Now" and "Restart Later" have a keyboard accelerator. If this dialog pops up, while I am pressing ALT+N, the current Windows session will be terminated immediately. And there is no Postpone button either. This button appears in Windows Vista, but there is no option "Do not prompt me again, I will restart my computer later.". If I do not intend to restart my computer very soon, I have to press "Postpone" in every 4 hours. It is really annoying! Not only the unfriendly design, but also infinite critical Windows updates. The only one option for me is to turn off "Automatic Updates".

According to the great design principles [2]: I suggest remove those keyboard accelerators and add a 10-second-delay before restarting Windows, so that user can cancel this action. This is called user experience!

References

  1. Gentle reminder: On a dialog box, do not give OK and Cancel accelerators
  2. How to Design a Great User Experience
 

Monday, May 26, 2008

Form disappeared

After I started working on dual monitors, I have found another interesting issue:

  1. Create a new application with a main form and a sub-form.
  2. Set the property Position of the sub-form to poMainFormCenter
  3. Create a button "Show Sub-Form" on main form.
  4. Move more than 50% area of the main form out of the main screen.
  5. Click the button, we created.
The sub-form disappeared! Because the popup position of sub-form is out of the screen. I did a similar test with Internet Explorer. Its dialog will be shown at the corner of the screen. It can be regarded as a bug, so I have reported this issue to QualityCentral [1] as well.

Solution

This time you have to modify Forms.pas
  1. Copy Forms.pas to your project folder,
  2. find and replace with the following code,
  3. and then recompile your project again.

{$IFDEF FIXUP_FORM_POPUP_POSITION}
function GetPrimaryMonitor: TMonitor;
var
I: Integer;
begin
for I := 0 to Screen.MonitorCount - 1 do
begin
Result := Screen.Monitors[I];
if Result.Primary then
Exit;
end;
Result := Screen.Monitors[0];
end;

procedure CenterFormToPrimaryMonitorCenter(var X, Y: Integer;
AForm: TCustomForm);
var
R: TRect;
begin
R := GetPrimaryMonitor.WorkareaRect;
X := R.Left + (R.Right - R.Left - AForm.Width) div 2;
Y := R.Top + (R.Bottom - R.Top - AForm.Height) div 2;
end;

procedure SnapFormToMonitorEdgeOnDemand(var X, Y: Integer;
AForm: TCustomForm);
var
DistanceLeft, DistanceRight: Integer;
CenterMon: TMonitor;
R: TRect;
begin
CenterMon := Screen.MonitorFromPoint(
Point(X + AForm.Width div 2, Screen.DesktopHeight div 2));
R := CenterMon.WorkareaRect;
// Adjust X-pos
DistanceLeft := X - R.Left;
DistanceRight := R.Right - (X + AForm.Width);
if (DistanceLeft < 0) and (DistanceRight < 0) then
begin
if DistanceLeft >= DistanceRight then
X := R.Left // Snap to left edge
else X := R.Right - AForm.Width; // Snap to right edge
end
else if (DistanceLeft < 0) or (DistanceRight < 0) then
begin
if DistanceLeft < 0 then
X := R.Left // Snap to left edge
else X := R.Right - AForm.Width; // Snap to right edge
end;
// Adjust Y-pos
if Y < R.Top then
Y := R.Top
else if Y > R.Bottom - AForm.Height then
Y := R.Bottom - AForm.Height;
end;
{$ENDIF}

procedure TCustomForm.CMShowingChanged(var Message: TMessage);
// ...
if (FPosition = poScreenCenter) or
((FPosition = poMainFormCenter) and (FormStyle = fsMDIChild)) then
begin
if FormStyle = fsMDIChild then
begin
X := (Application.MainForm.ClientWidth - Width) div 2;
Y := (Application.MainForm.ClientHeight - Height) div 2;
end else
begin
{$IFDEF FIXUP_FORM_POPUP_POSITION}
CenterFormToPrimaryMonitorCenter(X, Y, Self);
{$ELSE}
X := (Screen.Width - Width) div 2;
Y := (Screen.Height - Height) div 2;
{$ENDIF}
end;
{$IFDEF FIXUP_FORM_POPUP_POSITION}
SnapFormToMonitorEdgeOnDemand(X, Y, Self);
SetBounds(X, Y, Width, Height);
//SetWindowToMonitor() will cause unexpected popup position change!
{$ELSE}
if X < Screen.DesktopLeft then
X := Screen.DesktopLeft;
if Y < Screen.DesktopTop then
Y := Screen.DesktopTop;
SetBounds(X, Y, Width, Height);
if Visible then SetWindowToMonitor;
{$ENDIF}
end
else if FPosition in [poMainFormCenter, poOwnerFormCenter] then
begin
CenterForm := Application.MainForm;
if (FPosition = poOwnerFormCenter) and (Owner is TCustomForm) then
CenterForm := TCustomForm(Owner);
if Assigned(CenterForm) then
begin
X := ((CenterForm.Width - Width) div 2) + CenterForm.Left;
Y := ((CenterForm.Height - Height) div 2) + CenterForm.Top;
end else
begin
{$IFDEF FIXUP_FORM_POPUP_POSITION}
CenterFormToPrimaryMonitorCenter(X, Y, Self);
{$ELSE}
X := (Screen.Width - Width) div 2;
Y := (Screen.Height - Height) div 2;
{$ENDIF}
end;
{$IFDEF FIXUP_FORM_POPUP_POSITION}
SnapFormToMonitorEdgeOnDemand(X, Y, Self);
SetBounds(X, Y, Width, Height);
//SetWindowToMonitor() will cause unexpected popup position change!
{$ELSE}
if X < Screen.DesktopLeft then
X := Screen.DesktopLeft;
if Y < Screen.DesktopTop then
Y := Screen.DesktopTop;
SetBounds(X, Y, Width, Height);
if Visible then SetWindowToMonitor;
{$ENDIF}
end
else if FPosition = poDesktopCenter then
// ...
end;

Conclusion

This issue is nothing. The motivation of this article [2] is to suggest you figure out the differences among DesktopRect, BoundsRect and WorkAreaRect. They are almost the same on one monitor environment. But they are totally different on dual monitors, especially when TaskBar is not at its default position.

References

  1. Related discuss at QualityCentral.
  2. The Chinese version of this article (on my blog @csdn)
 

Sunday, April 27, 2008

A not cool thing of TCoolBar

There is a demo project "coolstuf" in Delphi folder. It demonstrates how to build your own web browser with Delphi. Unfortunately it shows you how to re-procedure an issue of TCoolBar as well. "Just drag the (third) Links toolbar to top and then to bottom, repeat it for several times, you will see an extra area (roughly 40px) at bottom of TCoolBar." I call this as a "TCoolBar Bands' Position Problem". If you do not get it, please look at the snapshot.


Snapshot (Red rectangle is the extra area)

If you have read other posts in my blog, you might know, that I am very uncomfortable with such UI problems. So I did some investigation: This issue take place, when one or more bands have a fixed size. (TCoolBand.FixedSize is True) I suppose, that the height of this (fixed-size) band was not updated, when TCoolBar's height was changed. Actually, this is not the only one issue of REBAR controls. For instance: No chevron support, no whole dropdown button support, flickering problem, etc. (I will talk about them later in another post)

Solution

I have reported this issue to QualityCentral [1]. Not like other patches I have made, this time, the solution is not clean enough.

You have to:
  1. Copy ComCtrls.pas to your project folder,
  2. find and replace with the following code,
  3. and then recompile your project again.


procedure TCoolBar.CNNotify(var Message: TWMNotify);
//PATCH-BEGIN
var
I, FID: Integer;
RBBI: TReBarBandInfo;
//PATCH-END
begin
if (Message.NMHdr^.code = RBN_HEIGHTCHANGE) then
if IsAutoSized and (ComponentState * [csLoading, csDestroying] = [])
then begin
//PATCH-BEGIN
for I := 0 to Bands.Count - 1 do
if Bands[I].FixedSize then
begin
FillChar(RBBI, SizeOfReBarBandInfo, 0);
RBBI.cbSize := SizeOfReBarBandInfo;
RBBI.fMask := RBBIM_STYLE;
FID := Bands[I].FID;
Perform(RB_GETBANDINFO, FID and IDMask, Integer(@RBBI));
Perform(RB_SETBANDINFO, FID and IDMask, Integer(@RBBI));
end;
//PATCH-END

ReadBands;
BeginUpdate;
try
if AutoSize then AdjustSize;
finally
EndUpdate;
end;
end
else if IsBackgroundDirty then
Invalidate;
end;

Conclusion

As I mentioned before, REBAR controls (TCoolBar, TCoolBand, TToolBar and TToolButton) have not been implemented correctly as they should be. I did some experiments and made couple of patches. This article [2] is only the part one. There will be part two, part three...

To note that some patches must modify interface declaration and implementation both. Unfortunately Delphi cannot re-compile those modified units, when their interface declaration has been changed. The only one solution, so far as I known, is to create a copy of the whole REBAR control set and modify the new set. I will be appreciated, if someone can suggest any better ways.

References

  1. Related discuss at QualityCentral
  2. The Chinese version of this article (on my blog @csdn)
 

Wednesday, April 9, 2008

Why Safari?

Why so many people love Mac OS? Is it really fantastic or just the way to express their /unique/ personality? These years I struggled with myself for buying a Mac or not. As I do not have deep pockets, a simple start can be trying its browser Safari on my windows box...

"A good name is a good start for a product." After couple of days' testing, my word was proved: Just like its name, Safari is "suffering". (If I am wrong, please correct me.)

First of all, there are two good things I mentioned:

  1. The fonts (except Chinese fonts) are perfect, especially the anti-aliasing effect has impacted me deeply. Text on web pages looks more crisper and easier to read. Although I saw one similar tool to increase the anti-aliasing for Windows, but I prefer to the results of Safari.
  2. When I search some text in a Web page, all matches will be highlighted with an extra large size and in a high contrast color, so that you can find your target easily.

All other experiences of Safari are really nightmares:
  1. There is no "New Tab" button on toolbar. Although user can double click on empty area of tab bar, like other browsers. But by default, when there is only one tab opened, tab bar is invisible. Shall I always stick my fingers on keyboard (to press CTRL+T)? Alternatively, user can also open a tab by middle-button clicking on a link. But it is not handy, why so complicated?
  2. The user interface looks material silver. All buttons especially the close tab "X" on each tab are not easy to be identified. By switching tabs I often close a web page by mistake. Even worse is the function "Reopen Last Closed Window" is belt in menu. I get really nervous while switching tabs. One thing to be noted: When multiple Safari instances are running, it might recover the last closed web page of previous instance, not the current.
  3. There is no drop-down button on address bar. When I try to open a frequently visited website, I have to type several chars and then select it from the list. Hui, my fingers will be permanently damaged after using Safari for one month.
  4. The memory usage is huge. Just run Safari will manipulate ca. 40MB memory. After navigating several simple websites, the usage will increase to 100MB-200MB or more. I did the same test in MSIE or FireFox. They use less.

Conclusion

Safari is suffering. Most problems (I regard as) are related to usability. Apple is proud of its design. But at least Safari disappointed me. I did not see too many highlights on it. I will still keep this toy for web page compatibility tests and hope something really exciting in next major release.
 

Thursday, January 10, 2008

Random thoughts on Unicode

Recently CodeGear engineers started to talk about Unicode support [1][2][3][4][5] in the next major release of Delphi (codenamed Tiburon [6]). I am still so excited to hear about that, although it should have been done ten years ago.

Is Unicode support a NP-problem?

You might ask the same question. In fact, a set of Wide-string types have been introduced for many years. But their RTL, VCL and IDE stay in Ansi-version stage. Why not update? IMO: 50% is because of technical issues and 50% is because of their market focus and operative issues. On one hand, a smooth migration is a big challenge, which must be tested completely and seriously. On another hand, their human resource might be limited, so that Unicode support has been shifted several times.

How about the days without Unicode support?

Fortunately, some nice guys have created some nice components for us in the most difficult days. The TntControls [7], a collection of basic Unicode enabled RTL and VCL, is one of them. The concept behind TntControls is to override all Ansi-version of string properties and routines with wide-versions. For Win9x platforms, all Wide-something will be casted to Ansi-something at runtime, so that everything works fine for all Windows platforms. But you have to create Wide-version of components and functions one by one. This is time intensive, boring and sometimes a little bit difficult. Furthermore, WideString is not reference counted. It performs /much more/ slower [8] than AnsiString. Unfortunately, nowadays there are few options...

What will be done to next Delphi?

First, a reference-counted new string type UnicodeString will be introduced. Second, all type aliases and function aliases will be switched from Ansi-version to Wide-version. So that most existed projects can be upgraded to Unicode stage without difficulties or performance lost. It sounds very simple, doesn't it? Thanks for those genies in advance.

But there are also two things I do not like:
  1. The new type name is inconsistent with existed types. AnsiString is equivalent to UnicodeString, but AnsiChar is equivalent to WideChar. I suggest deprecating WideChar as well and introducing a new type UnicodeChar.
  2. Type aliases and function aliases are not switchable, which means, that you have to make sure that UnicodeString will NOT break your code. Unfortunately, if your project is not test driven, it is very hard to say...

Conclusion

So many questions, discusses and requests about the new UnicodeString. No doubt, CodeGear engineers will be quite busy this year. The fully Unicode support is a big challenge to everyone. Are you ready? [9]

References

  1. Chris Bensen: Unicode
  2. Chris Bensen: Unicode: SizeOf is Different than Length Part II
  3. Chris Bensen: Unicode: SizeOf is Different than Length
  4. The Oracle at Delphi: DPL & Unicode - a toss up
  5. The Oracle at Delphi: More FAQs about Unicode in Tiburón
  6. Delphi and C++ Builder Roadmap
  7. TMSUnicode Components (formal TntControls)
  8. Tobias Gurock: What’s wrong with Delphi’s WideString?
  9. The Chinese version of this article (on my blog @csdn)
 

Potential memory leaks by initializing a record

Delphi uses reference-counting with copy-on-write semantics [1][2] to reduce memory allocation for strings (not for WideString). A kind of memory leak was found by accident. Let us first look the following example:

type
TFoo = record
StringField: string;
end;

procedure CreateLeakTest;
var
Foo: TFoo;
begin
FillChar(Foo, SizeOf(Foo), 0);
Foo.StringField := 'Leak Test';
FillChar(Foo, SizeOf(Foo), 0); //<--- A leak!
end;

Initializing records with FillChar() is quite common in Delphi. By calling FillChar() twice, you might create a memory leak. Note that no leaks on ShortString. My assumption: FillChar() is unsafe to cleanup records with ref-counted fields.

function StringStatus(const S: string): string;
begin
Result := Format('Addr: %p, Refc: %d, Val: %s',
[Pointer(S), PInteger(Integer(S) - 8)^, S]);
end;

procedure Diagnose;
var
S: string;
Foo: TFoo;
begin
S := Copy('Leak Test', 1, 5); // Force to allocate a new string
WriteLn(StringStatus(S));
Foo.StringField := S;
WriteLn(StringStatus(Foo.StringField));
FillChar(Foo, SizeOf(Foo), 0);
WriteLn(StringStatus(S));
end;

The output of the above code looks as follows:
Addr: 00E249E8, Refc: 1, Val: Leak Test // A string buffer is allocated
Addr: 00E249E8, Refc: 2, Val: Leak Test // Its Refc is incremented
Addr: 00E249E8, Refc: 2, Val: Leak Test // Its Refc should equal 1 (unexpected)

After calling FillChar(), the StringField is pointed to nil. However the reference count of its previous string buffer hasn't been decremented, so that its reference count will NEVER go back to 0. In other words, this string buffer will not be deallocated before your program is terminated. This is a leak.

How to initialize a record in a safe way?

As ref-counted fields are not handled correctly by using FillChar(). The default way of initializing a record looks more like an abuse of FillChar. I suggest declare a const record with initial values instead of using FillChar.

const
EmptyRecordX: TRecordX = (
Field1: InitVal1;
Field2: InitVal2;
...
FidldN: InitValN
);

// In your application
var
Foo: TRecordX;
begin
Foo := EmptyRecordX; // instead of FillChar(Foo, SizeOf(Foo), 0);
//...

It is quite safe to initialize a record in this way, isn't it?

Alternative Solution

If you are too lazy to declare such empty record constants. The following function can help you as well. Note that it is a little bit tricky.

procedure InitRecord(out R; RecordSize: Integer);
begin
FillChar(R, RecordSize, 0);
end;

Thanks for the magic word "out". As it is in Help described "An out parameter, like a variable parameter, is passed by reference. With an out parameter, however, the initial value of the referenced variable is discarded by the routine it is passed to. The out parameter is for output only; that is, it tells the function or procedure where to store output, but doesn't provide any input. "

Let us see what the code actually has done to a record.

mov edx,[$0040c904]
mov eax,ebx
call @FinalizeRecord //<----- cleanup
mov edx,$0000000c
call InitializeRecord

Compile calls procedure FinalizeRecord(), so that a record will be completely finalized.

UPDATE #1: As Jonas Maebe recently described: "If you have local record which was declared but not yet used, a simple fillchar(rec,sizeof(rec),0) will set everything to 0/nil/empty. If it may have been used earlier and contains ref-counted fields, you first have to call finalize(rec). "[3] His argument is more understandable and closer to the point of the issue.

References

  1. Wikipedia: Copy-on-write
  2. A Brief History of Strings
  3. fpc-pascal maillist
 

Way back into native: Tooltip

I love Delphi because of its components, I hate Delphi also because of its components. If you really care about user interface, you might find many minor differences between a standard Windows application and a Delphi application. For instance: All shortcuts of menu items are right aligned, when the menu is associated with an image list. When you press ALT, the first button on menubar should be selected. Missing support of chevron on toolbar. The tooltip (known as HintWindow in Delphi) looks different from the native tooltip. In this article I will started with tooltip and tell you how to make it nicer.

Background

In WindowsXP, even you are very careful, you might probably not mention the difference. A Delphi styled tooltip has a gray edge. (Actually it should be black) And there is no shadow under a tooltip either. This issue was not a real issue, until you upgrade to Windows Vista. A native tooltip in Vista [1][2] has rounded corners. Its background is gradient filled. (See the picture below)


Delphi style   vs.   Windows native style

No matter there is a manifest or not, you cannot change a tooltip to its native style. Why? I suppose, that Delphi engineers want to make the HintWindow more customizable, but it is quite difficult to start with the standard TOOLTIPS_CLASS, so they created a HintWindow as a WS_EX_TOOLWINDOW. Now it is easy to build your own styled tooltips, but it is also difficult to reproduce the native style, isn't it?

Solution

The idea is simple: if you do not intend to customize your tooltip, you can replace WS_EX_TOOLWINDOW with TOOLTIPS_CLASS. I have made a patch. You just have to include the NativeHintWindow.pas in your application and build your project again. Done, your nice tooltip is back. If you are using TntControls (or TMSUnicode Controls), there is an extra editon for you. Click here to download. It has been reported to QualityCentral [3] as well.

Conclusion

In this article [4], I have shown a visibility's issue of tooltip control (THintWindow) and have also implemented a patch to fix it. I hope Delphi engineers could pay more attention on such kind of issues. More posts about inconsistencies of user interface are coming soon. So stay tuned ;-)

References

  1. MSDN: Tooltips and Infotips
  2. MSDN: Top Guidelines Violations
  3. Related discuss at QualityCentral
  4. The Chinese version of this article (on my blog @csdn)

Performance issue of TAction

If you have never heard about TAction or used it, you are definitely not a Delphi developer ^^) This component simplifies the UI work. I mentioned once accidentally, that the CPU usage became abnormally high, when mouse was moved quickly on the main form. Spy++ tells, that massive WM_UPDATE message were sent, when mouse was moving fast over the main form. So I took a closer look into the details and found out that TContainedAction.Update() was executed many times by the TActionManager. As it is described in Help "this method triggers the OnUpdate event handler. ... When the application is idle, the OnUpdate event occurs for every action." The idle status will be changed very frequently. I have more than 100 TAction controls on the main form, which means TContainedAction.Update() was executed more than 100 times in a very short time. This explains, why my application became a CPU usage monster.

Solution

If your application does not handle any OnUpdate events, it really makes sense to accelerate TContrainedAction.Update().

My solution is to replace the original method is an empty method.

uses
FastcodePatch {MPL http://fastcode.sourceforge.net/};

procedure TContainedActionUpdateStub;
asm
call TContainedAction.Update;
end;

type
TContainedActionPatch = class(TContainedAction)
public
function Update: Boolean; override;
end;

function TContainedActionPatch.Update: Boolean;
begin
Result := False;
end;

// Disallows the TContainedAction.Update to trigger TAction.OnUpdate()
procedure DisableTContainedActionUpdate;
begin
FastcodeAddressPatch(
FastcodeGetAddress(@TContainedActionUpdateStub),
@TContainedActionPatch.Update);
end;

The best place to run this patch is in YourForm.OnCreate() event. If you want to make a permanent patch, you can either modify TContainedAction.Update in ActnList.pas directly, or submit it to CodeGear's quality center.

Conclusion

In this article [1], I have shown a potential performance issue by using massive TxxxxAction components. I have also implemented a patch to fix this issue. Here is the patch

References

  1. The Chinese version of this article (on my blog @csdn)