在消失4个版本之后,类向导终于重返Visual C++。新的功能:搜索现在可以部分匹配而不是从字符串开始匹配。
测试版还是存在一些问题,向导不是总能找到现存的函数,以致删除函数功能不是总有效。在打开很多文档的时候尝试打开Class Wizard会出现“value does not fall in expected range” 错误。再就是性能问题,打开的文档越多,类向导启动所需的时间就越长。
Download the code for this article (22KB)
.Net 2.0 introduced autocompletion in TextBox and ComboBox. It is obvious that autocomplete is not very useful when the number of options is small. However, when your option becomes too many, pre-filling of all options to an AutoCompleteStringCollection becomes impractical, especially when your data comes from a remote computer. An alternative is to replace the AutoCompleteCustomSource in a TextChanged event, however, users are getting random AccessViolationException when trying to replace it. In this article I will demonstrate another alternative, using a BindingSource as the data source of options and bypassing the .Net framework and call the underline Windows API directly.
The first thing I need to do is to port the APIs to managed code. The autocompletion API is exposed as a COM object, so I need to write managed version of its interfaces:
[ComImport] [InterfaceType(ComInterfaceType::InterfaceIsIUnknown)] [Guid("EAC04BC0-3791-11D2-BB95-0060977B464C")] [SuppressUnmanagedCodeSecurity] interface class IAutoComplete2 { [PreserveSig] int Init( HandleRef hwndEdit, IEnumString^ punkACL, String^ pwszRegKeyPath, String^ pwszQuickComplete ); void Enable( [MarshalAs(UnmanagedType::Bool)] bool value);
int SetOptions(int dwFlag);
void GetOptions([Out]IntPtr pdwFlag); };Second, I need to create the autocomplete object and query the IAutoComplete2 interface:
Type^ autoCompleteType = Type::GetTypeFromCLSID(CLSID_AutoComplete); try{ autoComplete2 =(IAutoComplete2^)(Activator::CreateInstance(autoCompleteType)); } catch(Exception^ e) { Marshal::ReleaseComObject(autoComplete2); autoComplete2 = nullptr; }
Third, I need to bind it to an TextBox control:
bool AutocompleteBindingSource::Bind() { if (nullptr==this->autoComplete2) return false; try { this->autoComplete2->SetOptions((int)ControlToBind->AutoCompleteMode); this->autoComplete2->Init( HandleRef(ControlToBind,ControlToBind->Handle), this, String::Empty ,String::Empty); return true; } catch(Exception^e) { return false; }
}
Finally, I need to implement IEnumString to provide a list of options. Luckily, .Net has declared this interface, so I don't need to port it to managed code, however, I still need to write my binding code in my implementation of IEnumString.
void AutocompleteBindingSource::Reset() { this->current = 0; if(BindingSource!=nullptr) this->size=BindingSource->Count; }
int AutocompleteBindingSource::Next( int celt, [Out, MarshalAs(UnmanagedType::LPArray, ArraySubType=UnmanagedType::LPWStr, SizeParamIndex=0)] array<String^>^ rgelt, IntPtr pceltFetched) { if (celt < 0) { return E_INVALIDARG; } int index = 0; while ((this->current < this->size) && (celt > 0)) { Object^ item=this->BindingSource->default[this->current];
bool useDisplayMember=false;
if(!String::IsNullOrEmpty(DisplayMember)) { ICustomTypeDescriptor^ customTypeDescriptor=dynamic_cast<ICustomTypeDescriptor^>(item); if(customTypeDescriptor!=nullptr) { PropertyDescriptorCollection^ propertyDescriptorCollection= customTypeDescriptor->GetProperties(); if(propertyDescriptorCollection!=nullptr) { PropertyDescriptor^ propertyDescriptor=propertyDescriptorCollection->default[DisplayMember]; if(propertyDescriptor!=nullptr) { rgelt[index] = propertyDescriptor->GetValue(item)->ToString(); useDisplayMember=true; } } } }
if(!useDisplayMember) { if(item!=nullptr) { rgelt[index] = item->ToString(); } } this->current++; index++; celt--; } if ((pceltFetched != IntPtr::Zero)) { Marshal::WriteInt32(pceltFetched, index); } if ((celt != 0)) { return 1; } return 0;
Here the DisplayMember property is the name of the property in the data source to be displayed. If the property specified by the value of the DataMember property does not exist, I use ToString to get a text representation of the current item in the data source.
You may want to ask, where is the filtering code? Well, that is implemented by BindingSource class.
System::Void FormTest::textBoxDemo_TextChanged(System::Object^ sender, System::EventArgs^ e) { static bool inThisFunction=false; if(!inThisFunction) { inThisFunction=true; if(String::IsNullOrEmpty(textBoxDemo->Text)) bindingSourceAutoComplete->Filter=nullptr; else { System::String^ addText=textBoxDemo->Text+"og/NextElement"; dataSetDemo->Tables[0]->DefaultView->Sort="Text"; if(dataSetDemo->Tables[0]->DefaultView->FindRows(addText)->Length==0) { System::Data::DataRow^ row=dataSetDemo->Tables[0]->NewRow(); row->default[0]=addText; dataSetDemo->Tables[0]->Rows->Add(row); } bindingSourceAutoComplete->Filter= String::Format("{0} LIKE '{1}%'" ,dataSetDemo->Tables[0]->Columns[0]->Caption ,textBoxDemo->Text); } if(textBoxDemo->SelectionStart>0) { autocompleteBindingSource1->Reset(); autocompleteBindingSource1->Bind(); String^ text=textBoxDemo->Text; int selectionStart=textBoxDemo->SelectionStart; int selectionLength=textBoxDemo->SelectionLength; textBoxDemo->SelectionStart=0; textBoxDemo->SelectionLength=0; textBoxDemo->SelectAll(); System::Windows::Forms::SendKeys::SendWait("{BACKSPACE}"); textBoxDemo->Text=text; textBoxDemo->SelectionStart=selectionStart-1; textBoxDemo->SelectionLength=selectionLength+1; System::Windows::Forms::SendKeys::SendWait(textBoxDemo->SelectedText); } inThisFunction=false; } }
Somehow Windows caches the result. If I don't clear the text, my IEnumString implementation won't be asked again for candidate strings (pointed out by Andy Gilman).
The BindingSource class checks the data source to see if they support the IBindingListView. If IBindingListView is supported, the BindingSource class delegates sorting and filtering to the data source. In this sample, the data source of the BindingSource object is a DataSet, and the DataMember of BindingSource object is the name of the first table , so BindingSource creates a DataView as its data source. The DataView class implements IBindingListView and filters its data using expressions parsed from the filter string. In reality, the data source could be a business object that implements IBindingListView and supports filtering and sorting with stored procedures.
This sample does not consider compound autocomplete object support. If you want to get your options from multiple sources, you need to use IObjMgr to add sources to the autocomplete object.
Reference
Visual Studio 2008 Beta2中的Class Designer终于支持C++了,上面是一个MFC程序的类图,可以看到已经支持扩展MFC的宏了,可惜只能看不能重构代码。尽管Class Designer这功能相当不错,但是设计师们可能还是更习惯IBM 的Rational Rose Developer for Visual Studio和UML。我用Class Designer的C#支持的时候也就是加加注释而已,重构我更习惯用DevExpress提供的工具Refactor来做,类则用XSD.exe生成,因为Class Designer生成的属性只会扔NotImplementedException异常。
Visual C++项目组在做下一个版本的市场调查,有兴趣的可以去提提要求。
Download: http://jiangsheng.net/Documents/IE8AddZone.zip
Prerequisite: Microsoft Visual C++ 2005 SP1 Redistributable Package (x86)
This program is designed to temporary circumvent the "An add-on for this web site failed to run. Check the security settings in Internet options for potential conflicts" error after installing IE8. For more details about this problem, visit https://connect.microsoft.com/VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=425510
Update: the VC team's workaround is at http://blogs.msdn.com/vcblog/archive/2009/03/28/some-vs2005-and-vs2008-wizards-pop-up-script-error.aspx. However, I still suggest copying other settings from a restricted zone before modify the 1207 key.
Clicking the create button will create another Internet Explorer security zone with the id 1000 that will affect all urls not included in other zones. This will allow separate security settings for webbrowser control hosting application such as Visual Studio. If settings are copied from a trusted configuration such as "my computer",a webbrowser control hosting application may not experience the aforementioned error.
Security Remarks: do not visit unsecured web site or use email software on the same computer after creating the new zone as the security impact of the new zone is not yet tested. Applications that host webbrowser control should implement their own security manager if they want to run ActiveX in the browser control.
If you received a fix from the IE team for this problem, please delete the zone created by this problem by clicking the delete button. Visit https://connect.microsoft.com/VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=425510 for the status of this problem.
For more information about adding zones to IE, visit http://www.nthelp.com/50/addazone.htm .
For more information about Internet Explorer security zones, visit http://support.microsoft.com/kb/182569
For more information about Visual C++ wizards, visit http://msdn.microsoft.com/en-us/library/aa730846(VS.80).aspx
Suspicious url actions from Regmon logs:
In Visual C# 2005 SP1, I added an object data source to a web page that uses my business class as the select method. The method has one parameter of type Guid. The data source wizard generates code like this
<asp:Parameter DbType="Guid" Name="rowId" />
Although the web server has .Net 2.0 SP1 installed (I checked the registry), it still throws an error
Type 'System.Web.UI.WebControls.Parameter' does not have a public property named 'DbType'
The walk around is easy:
<asp:Parameter Type="Object" Name="rowId" />