逐步解說:擴充資料庫重新命名重構以處理文字檔
在本逐步說明主題中,您將建立、安裝、註冊和測試新的重新命名重構參與者。 此重構「目標」(Target) 將擴充 Visual Studio Premium 或 Visual Studio Ultimate 的功能,可讓您的「資料庫重構」(Database Refactoring) 重新命名您的「資料庫專案」(Database Project) 中文字檔所含「資料庫物件」(Database Object) 的參考。
當您將新的重構參與者加入至現有類型的重構時,它必須使用現有的參與者輸入類別。
這個逐步解說將說明下列工作:
建立新組件,其中包含自訂重構目標的類別。
安裝並註冊組件,以便在 Visual Studio Premium 或 Visual Studio Ultimate 中使用重構目標。
建立簡單的資料庫專案來測試重構目標是否如預期運作。
必要條件
您需要下列元件才能完成此逐步解說:
您必須已安裝 Visual Studio 2010 Premium 或 Visual Studio 2010 Ultimate。
您也必須在電腦中安裝 Visual Studio 2010 SDK。 若要下載這個套件,請參閱 Microsoft 網站的網頁:Visual Studio 2010 SDK (英文)。
建立具有自訂重構目標的組件
若要建立自訂重構目標,讓重新命名重構處理文字檔,您必須實作一個類別來提供新的 RefactoringContributor:
- RenameReferenceTextContributorContributor - 此類別會針對重新命名的符號建置變更提議清單。 這些變更提議適用於資料庫專案中文字檔所包含的每個參考。
建立此類別之前,您將建立類別庫、加入必要的參考,以及加入一些 Helper 程式碼,以簡化您將在本逐步解說稍後撰寫的部分程式碼。
若要建立類別庫和 Helper 程式碼
建立新的 C# 類別庫專案並命名為 RenameTextContributor.csproj。
加入下列 .NET 組件的參考:
Microsoft.Data.Schema
Microsoft.Data.Schema.ScriptDom
Microsoft.Data.Schema.ScriptDom.sql
Microsoft.Data.Schema.Sql
加入下列組件 (可在 %Program Files%\Microsoft Visual Studio 10.0\VSTSDB 資料夾中找到) 的參考。
Microsoft.Data.Schema.Tools.Sql.dll
Microsoft.VisualStudio.Data.Schema.Package.dll
Microsoft.VisualStudio.Data.Schema.Package.Sql.dll
將來自 Visual Studio 2010 軟體開發套件 (SDK) 的參考加入至下列組件:
Microsoft.VisualStudio.OLE.Interop.dll
Microsoft.VisualStudio.Shell.10.0.dll
Microsoft.VisualStudio.TextManager.Interop.dll
Microsoft.VisualStudio.Shell.Interop.dll
Microsoft.VisualStudio.Shell.Interop.8.0.dll
Microsoft.VisualStudio.Shell.Interop.9.0.dll
Microsoft.VisualStudio.Shell.Interop.10.0.dll
在 [方案總管] 中,將 Class1.cs 重新命名為 SampleHelper.cs。
按兩下 SampleHelper.cs,在程式碼編輯器中開啟它。
注意事項 這個協助程式類別與自訂重構類型逐步解說中使用的 Helper 文字相同。 您可以將該專案的原始程式碼複製到新專案以節省時間。
以下列程式碼取代程式碼編輯器的內容:
using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Runtime.InteropServices; using Microsoft.Data.Schema.SchemaModel; using Microsoft.Data.Schema.ScriptDom.Sql; using Microsoft.VisualStudio; using Microsoft.VisualStudio.Data.Schema.Package.Refactoring; using Microsoft.VisualStudio.Data.Schema.Package.UI; using Microsoft.VisualStudio.Shell.Interop; using Microsoft.VisualStudio.TextManager.Interop; namespace MySamples.Refactoring { internal static class SampleHelper { public static String GetModelElementName(IModelElement modelElement) { SampleHelper.CheckNullArgument(modelElement, "modelElement"); return modelElement.ToString(); } /// <summary> /// Given a model element, returns its simple name. /// </summary> public static String GetModelElementSimpleName(IModelElement modelElement) { String separator = "."; String simpleName = String.Empty; String fullName = modelElement.ToString(); String[] nameParts = fullName.Split(separator.ToCharArray(), StringSplitOptions.RemoveEmptyEntries); if (nameParts.Length > 0) { simpleName = nameParts[nameParts.Length - 1]; // last part } if (simpleName.StartsWith("[") && simpleName.EndsWith("]")) { simpleName = simpleName.Substring(1, simpleName.Length - 2); } return simpleName; } /// <summary> /// Find all files in the project with the specified file extension /// </summary> public static List<string> GetAllFilesInProject(IVsHierarchy solutionNode, string fileExtension, bool visibleNodesOnly) { List<string> files = new List<string>(); if (null != solutionNode) { EnumProjectItems(solutionNode, fileExtension, files, VSConstants.VSITEMID_ROOT, // item id of solution root node 0, // recursion from solution node true, // hierarchy is Solution node visibleNodesOnly); // visibleNodesOnly } return files; } /// <summary> /// Enumerates recursively over the hierarchy items. /// </summary> /// <param name="hierarchy">hierarchy to enmerate over.</param> /// <param name="fileExtension">type of files we need to collect from the project</param> /// <param name="files">list of file paths</param> /// <param name="itemid">item id of the hierarchy</param> /// <param name="recursionLevel">Depth of recursion. For example, if recursion started with the Solution /// node, then : Level 0 -- Solution node, Level 1 -- children of Solution, etc.</param> /// <param name="hierIsSolution">true if hierarchy is Solution Node. </param> /// <param name="visibleNodesOnly">true if only nodes visible in the Solution Explorer should /// be traversed. false if all project items should be traversed.</param> private static void EnumProjectItems(IVsHierarchy hierarchy, string fileExtension, List<string> files, uint itemid, int recursionLevel, bool hierIsSolution, bool visibleNodesOnly) { int hr; IntPtr nestedHierarchyObj; uint nestedItemId; Guid hierGuid = typeof(IVsHierarchy).GUID; // Check first if this node has a nested hierarchy. hr = hierarchy.GetNestedHierarchy(itemid, ref hierGuid, out nestedHierarchyObj, out nestedItemId); if (VSConstants.S_OK == hr && IntPtr.Zero != nestedHierarchyObj) { IVsHierarchy nestedHierarchy = Marshal.GetObjectForIUnknown(nestedHierarchyObj) as IVsHierarchy; Marshal.Release(nestedHierarchyObj); if (nestedHierarchy != null) { EnumProjectItems(nestedHierarchy, fileExtension, files, nestedItemId, recursionLevel, false, visibleNodesOnly); } } else { // Check if the file extension of this node matches string fileFullPath; hierarchy.GetCanonicalName(itemid, out fileFullPath); if (CompareExtension(fileFullPath, fileExtension)) { // add matched file paths into the list files.Add(fileFullPath); } recursionLevel++; //Get the first child node of the current hierarchy being walked object pVar; hr = hierarchy.GetProperty(itemid, ((visibleNodesOnly || (hierIsSolution && recursionLevel == 1) ? (int)__VSHPROPID.VSHPROPID_FirstVisibleChild : (int)__VSHPROPID.VSHPROPID_FirstChild)), out pVar); ErrorHandler.ThrowOnFailure(hr); if (VSConstants.S_OK == hr) { // Use Depth first search so at each level we recurse to check if the node has any children // and then look for siblings. uint childId = GetItemId(pVar); while (childId != VSConstants.VSITEMID_NIL) { EnumProjectItems(hierarchy, fileExtension, files, childId, recursionLevel, false, visibleNodesOnly); hr = hierarchy.GetProperty(childId, ((visibleNodesOnly || (hierIsSolution && recursionLevel == 1)) ? (int)__VSHPROPID.VSHPROPID_NextVisibleSibling : (int)__VSHPROPID.VSHPROPID_NextSibling), out pVar); if (VSConstants.S_OK == hr) { childId = GetItemId(pVar); } else { ErrorHandler.ThrowOnFailure(hr); break; } } } } } /// <summary> /// Gets the item id. /// </summary> /// <param name="pvar">VARIANT holding an itemid.</param> /// <returns>Item Id of the concerned node</returns> private static uint GetItemId(object pvar) { if (pvar == null) return VSConstants.VSITEMID_NIL; if (pvar is int) return (uint)(int)pvar; if (pvar is uint) return (uint)pvar; if (pvar is short) return (uint)(short)pvar; if (pvar is ushort) return (uint)(ushort)pvar; if (pvar is long) return (uint)(long)pvar; return VSConstants.VSITEMID_NIL; } /// <summary> /// Check if the file has the expected extension. /// </summary> /// <param name="filePath"></param> /// <param name="extension"></param> /// <returns></returns> public static bool CompareExtension(string filePath, string extension) { bool equals = false; if (!string.IsNullOrEmpty(filePath)) { equals = (string.Compare(System.IO.Path.GetExtension(filePath), extension, StringComparison.OrdinalIgnoreCase) == 0); } return equals; } /// <summary> /// Read file content from a file /// </summary> /// <param name="filePath"> file path </param> /// <returns> file content in a string </returns> internal static string ReadFileContent(string filePath) { // Ensure that the file exists first. if (!File.Exists(filePath)) { Debug.WriteLine(string.Format("Cannot find the file: '{0}'", filePath)); return string.Empty; } string content; using (StreamReader reader = new StreamReader(filePath)) { content = reader.ReadToEnd(); reader.Close(); } return content; } /// <summary> /// Check null references and throw /// </summary> /// <param name="obj"></param> /// <param name="?"></param> public static void CheckNullArgument(object obj, string objectName) { if (obj == null) { throw new System.ArgumentNullException(objectName); } } /// <summary> /// Get offset of the fragment from an Identifier if the identifier.value matches the /// name we are looking for. /// </summary> /// <param name="identifier"></param> /// <param name="expectedName"></param> public static RawChangeInfo AddOffsestFromIdentifier( Identifier identifier, String expectedName, String newName, Boolean keepOldQuote) { RawChangeInfo change = null; if (identifier != null && String.Compare(expectedName, identifier.Value, true) == 0) { if (keepOldQuote) { QuoteType newQuote = QuoteType.NotQuoted; newName = Identifier.DecodeIdentifier(newName, out newQuote); newName = Identifier.EncodeIdentifier(newName, identifier.QuoteType); } change = new RawChangeInfo(identifier.StartOffset, identifier.FragmentLength, expectedName, newName); } return change; } public static IList<ChangeProposal> ConvertOffsets( string projectFullName, string fileFullPath, List<RawChangeInfo> changes, bool defaultIncluded) { // Get the file content into IVsTextLines IVsTextLines textLines = GetTextLines(fileFullPath); int changesCount = changes.Count; List<ChangeProposal> changeProposals = new List<ChangeProposal>(changesCount); for (int changeIndex = 0; changeIndex < changesCount; changeIndex++) { int startLine = 0; int startColumn = 0; int endLine = 0; int endColumn = 0; RawChangeInfo currentChange = changes[changeIndex]; int startPosition = currentChange.StartOffset; int endPosition = currentChange.StartOffset + currentChange.Length; int result = textLines.GetLineIndexOfPosition(startPosition, out startLine, out startColumn); if (result == VSConstants.S_OK) { result = textLines.GetLineIndexOfPosition(endPosition, out endLine, out endColumn); if (result == VSConstants.S_OK) { TextChangeProposal changeProposal = new TextChangeProposal(projectFullName, fileFullPath, currentChange.NewText); changeProposal.StartLine = startLine; changeProposal.StartColumn = startColumn; changeProposal.EndLine = endLine; changeProposal.EndColumn = endColumn; changeProposal.Included = defaultIncluded; changeProposals.Add(changeProposal); } } if (result != VSConstants.S_OK) { throw new InvalidOperationException("Failed to convert offset"); } } return changeProposals; } /// <summary> /// Get IVsTextLines from a file. If that file is in RDT, get text buffer from it. /// If the file is not in RDT, open that file in invisible editor and get text buffer /// from it. /// If failed to get text buffer, it will return null. /// </summary> /// <param name="fullPathFileName">File name with full path.</param> /// <returns>Text buffer for that file.</returns> private static IVsTextLines GetTextLines(string fullPathFileName) { System.IServiceProvider serviceProvider = DataPackage.Instance; IVsTextLines textLines = null; IVsRunningDocumentTable rdt = (IVsRunningDocumentTable)serviceProvider.GetService(typeof(SVsRunningDocumentTable)); if (rdt != null) { IVsHierarchy ppHier = null; uint pitemid, pdwCookie; IntPtr ppunkDocData = IntPtr.Zero; try { rdt.FindAndLockDocument((uint)(_VSRDTFLAGS.RDT_NoLock), fullPathFileName, out ppHier, out pitemid, out ppunkDocData, out pdwCookie); if (pdwCookie != 0) { if (ppunkDocData != IntPtr.Zero) { try { // Get text lines from the doc data IVsPersistDocData docData = (IVsPersistDocData)Marshal.GetObjectForIUnknown(ppunkDocData); if (docData is IVsTextLines) { textLines = (IVsTextLines)docData; } else { textLines = null; } } catch (ArgumentException) { // Do nothing here, it will return null stream at the end. } } } else { // The file is not in RDT, open it in invisible editor and get the text lines from it. IVsInvisibleEditor invisibleEditor = null; TryGetTextLinesAndInvisibleEditor(fullPathFileName, out invisibleEditor, out textLines); } } finally { if (ppunkDocData != IntPtr.Zero) Marshal.Release(ppunkDocData); } } return textLines; } /// <summary> /// Open the file in invisible editor in the running /// documents table (RDT), and get text buffer from that editor. /// </summary> /// <param name="fullPathFileName">File name with full path.</param> /// <param name="spEditor">The result invisible editor.</param> /// <param name="textLines">The result text buffer.</param> /// <returns>True, if the file is opened correctly in invisible editor.</returns> private static bool TryGetTextLinesAndInvisibleEditor(string fullPathFileName, out IVsInvisibleEditor spEditor, out IVsTextLines textLines) { System.IServiceProvider serviceProvider = DataPackage.Instance; spEditor = null; textLines = null; // Need to open this file. Use the invisible editor manager to do so. IVsInvisibleEditorManager spIEM; IntPtr ppDocData = IntPtr.Zero; bool result; Guid IID_IVsTextLines = typeof(IVsTextLines).GUID; try { spIEM = (IVsInvisibleEditorManager)serviceProvider.GetService(typeof(IVsInvisibleEditorManager)); spIEM.RegisterInvisibleEditor(fullPathFileName, null, (uint)_EDITORREGFLAGS.RIEF_ENABLECACHING, null, out spEditor); if (spEditor != null) { int hr = spEditor.GetDocData(0, ref IID_IVsTextLines, out ppDocData); if (hr == VSConstants.S_OK && ppDocData != IntPtr.Zero) { textLines = Marshal.GetTypedObjectForIUnknown(ppDocData, typeof(IVsTextLines)) as IVsTextLines; result = true; } else { result = false; } } else { result = false; } } finally { if (ppDocData != IntPtr.Zero) Marshal.Release(ppDocData); } return result; } } }
按一下 [檔案] 功能表上的 [儲存 SampleHelper.cs]。
將名為 RawChangeInfo 的類別加入到專案。
在程式碼編輯器中,將程式碼更新為符合下列各行程式碼:
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace MySamples.Refactoring { /// <summary> /// Helper class to encapsulate StartOffset, FragmentLength and change string from /// parser and SchemaAnalzyer. /// </summary> internal sealed class RawChangeInfo { private int _startOffset; private int _length; private string _oldText; private string _newText; public RawChangeInfo(int startOffset, int length, string oldText, string newText) { _startOffset = startOffset; _length = length; _oldText = oldText; _newText = newText; } public int StartOffset { get { return _startOffset; } set { _startOffset = value; } } public int Length { get { return _length; } } public string OldText { get { return _oldText; } } public string NewText { get { return _newText; } set { _newText = value; } } } }
按一下 [檔案] 功能表上的 [儲存 RawChangeInfo.cs]。
接下來,您將定義 RenameReferenceTextContributor 類別。
若要定義 RenameReferenceTextContributor 類別
將名為 RenameReferenceTextContributor 的類別加入到您的專案。
在程式碼編輯器中,將 using 陳述式更新為符合下列程式碼:
using System; using System.Collections.Generic; using Microsoft.Data.Schema.Extensibility; using Microsoft.Data.Schema.Sql; using Microsoft.VisualStudio; using Microsoft.VisualStudio.Data.Schema.Package.Refactoring; using Microsoft.VisualStudio.Data.Schema.Package.Sql.Refactoring;
將命名空間變更為 MySamples.Refactoring:
namespace MySamples.Refactoring
將類別定義更新為符合下列各行程式碼:
[DatabaseSchemaProviderCompatibility(typeof(SqlDatabaseSchemaProvider))] internal class RenameReferenceTextContributor : RefactoringContributor<RenameReferenceContributorInput> { }
指定屬性來宣告此參與者與任何衍生自 SqlDatabaseSchemaProvider 的「資料庫結構描述提供者」(Database Schema Provider) 相容。 您的類別必須繼承自 RenameReferenceContributorInput 的 RefactoringContributor。
定義其他常數和私用成員變數:
#region const private const string TxtExtension = @".txt"; private const string PreviewFriendlyName = @"Text Files"; private const string PreviewDescription = @"Update text symbols in text files in the database project."; private const string PreviewWarningMessage = @"Updating text symbols in text files in the database project can cause inconsistency."; #endregion #region members private RefactoringPreviewGroup _textPreviewGroup; private List<String> _changingFiles; #endregion
常數提供將出現在 [預覽視窗] 中的資訊。 其他成員則用來追蹤預覽群組和將變更的文字檔清單。
加入類別建構函式:
#region ctor public RenameReferenceTextContributor() { _textPreviewGroup = new RefactoringPreviewGroup(PreviewFriendlyName); _textPreviewGroup.Description = PreviewDescription; _textPreviewGroup.WarningMessage = PreviewWarningMessage; _textPreviewGroup.EnableChangeGroupUncheck = true; _textPreviewGroup.EnableChangeUncheck = true; _textPreviewGroup.DefaultChecked = false; _textPreviewGroup.IncludeInCurrentProject = true; // This sample uses the default icon for the file, // but you could provide your own icon here. //RefactoringPreviewGroup.RegisterIcon(TxtExtension, "textfile.ico"); } #endregion
建構函式會初始化模型項目,建立新的預覽群組並初始化其屬性。 您也可以在這裡註冊圖示,以顯示在 [預覽視窗] 中來代表特定副檔名。 由於文字檔沒有語法反白顯示,因此您無須為文字檔註冊語言服務。
覆寫 PreviewGroup 屬性,以傳回建立此參與者時所建立的群組:
/// <summary> /// Preview group for text files /// </summary> public override RefactoringPreviewGroup PreviewGroup { get { return _textPreviewGroup; } set { _textPreviewGroup = value; } }
覆寫 ContributeChanges(Boolean) 方法以傳回變更提議清單:
/// <summary> /// Contribute to the change proposals /// </summary> /// <param name="input">contributor input</param> /// <returns>List of change proposals with corresponding contributor inputs</returns> protected override Tuple<IList<ChangeProposal>, IList<ContributorInput>> ContributeChanges(RenameReferenceContributorInput input) { RenameReferenceContributorInput referenceInput = input as RenameReferenceContributorInput; if (referenceInput == null) { throw new ArgumentNullException("input"); } string projectFullName; referenceInput.RefactoringOperation.CurrentProjectHierarchy.GetCanonicalName(VSConstants.VSITEMID_ROOT, out projectFullName); return GetChangesForAllTextFiles(referenceInput, projectFullName, _textPreviewGroup.DefaultChecked, out _changingFiles); }
這個方法會呼叫 GetAllChangesForAllTextFiles 方法來執行大部分的工作。
加入 GetChangesForAllTextFiles 方法來逐一查看專案中包含的文字檔清單、取得每個檔案的變更,以及將這些變更彙總至變更提議清單:
/// <summary> /// Get all changes from all text files. /// </summary> private static Tuple<IList<ChangeProposal>, IList<ContributorInput>> GetChangesForAllTextFiles( RenameReferenceContributorInput input, string projectFullName, bool defaultChecked, out List<String> changingFiles) { if (input == null) { throw new ArgumentNullException("input"); } changingFiles = new List<String>(); List<ChangeProposal> allChanges = new List<ChangeProposal>(); List<string> files = new List<string>(); files = SampleHelper.GetAllFilesInProject(input.RefactoringOperation.CurrentProjectHierarchy, TxtExtension, false); // process the text files one by one if (files != null && files.Count > 0) { int fileCount = files.Count; // Get all the changes for all txt files. for (int fileIndex = 0; fileIndex < fileCount; fileIndex++) { IList<ChangeProposal> changes = GetChangesForOneTextFile( input, projectFullName, files[fileIndex], defaultChecked); if (changes != null && changes.Count > 0) { allChanges.AddRange(changes); changingFiles.Add(files[fileIndex]); } } } return new Tuple<IList<ChangeProposal>, IList<ContributorInput>>(allChanges, null); }
實作 GetChangesForOneTextFileMethod 方法來傳回單一文字檔中包含的變更提議清單:
/// <summary> /// Get all the change proposals from one text file. /// </summary> private static IList<ChangeProposal> GetChangesForOneTextFile( RenameReferenceContributorInput input, string projectFullName, string fileFullPath, bool defaultChecked) { const string separators = " \t \r \n \\()[]{}|.+-*/~!@#$%^&<>?:;"; string fileContent = SampleHelper.ReadFileContent(fileFullPath); IList<ChangeProposal> changeProposals= null; if (string.IsNullOrEmpty(fileContent)) { // return empty change list changeProposals = new List<ChangeProposal>(); } else { int startIndex = 0; int maxIndex = fileContent.Length - 1; string oldName = input.OldName; int oldNameLength = oldName.Length; List<RawChangeInfo> changes = new List<RawChangeInfo>(); while (startIndex < maxIndex) { // Text files do not understand schema object information // We do just case-insensitive string match (without schema info) // Only match whole word int offset = fileContent.IndexOf(oldName, startIndex, StringComparison.OrdinalIgnoreCase); // Cannot find match any more, stop the match if (offset < 0) { break; } startIndex = offset + oldNameLength; // match whole word: check before/after characters are separators if (offset > 0) { char charBeforeMatch = fileContent[offset - 1]; // match starts in the middle of a token, discard and move on if (!separators.Contains(charBeforeMatch.ToString())) { continue; } } if (offset + oldNameLength < maxIndex) { char charAfterMatch = fileContent[offset + oldNameLength]; // match ends in the middle of a token, discard and move on if (!separators.Contains(charAfterMatch.ToString())) { continue; } } RawChangeInfo change = new RawChangeInfo(offset, oldNameLength, input.OldName, input.NewName); changes.Add(change); } // convert index-based offsets to ChangeOffsets in ChangeProposals changeProposals = SampleHelper.ConvertOffsets( projectFullName, fileFullPath, changes, defaultChecked); } return changeProposals; }
由於重構目標不是 Transact-SQL 指令碼或結構描述物件,因此這個方法不會使用 Microsoft.Data.Schema.ScriptDom 或 Microsoft.Data.Schema.Sql.SchemaModel 的型別或方法。這個重構目標會為文字檔提供尋找和取代實作,因為文字檔中的符號沒有結構描述資訊可用。
按一下 [檔案] 功能表上的 [儲存 RenameTextContributor.cs]。
接下來,您將設定和建置組件。
若要簽署和建置組件
按一下 [專案] 功能表上的 [RenameTextContributor 屬性]。
按一下 [簽署] 索引標籤。
按一下 [簽署組件]。
在 [選擇強式名稱金鑰檔] 中,按一下 [<新增>]。
在 [建立強式名稱金鑰] 對話方塊的 [金鑰檔名稱] 中,輸入 MyRefKey。
(選擇性) 您可以為強式名稱金鑰檔指定密碼。
按一下 [確定]。
在 [檔案] 功能表上按一下 [全部儲存]。
在 [建置] 功能表上,按一下 [建置方案]。
接下來,您必須安裝並註冊組件,以便出現在可用的「測試條件」(Test Condition) 中。
若要安裝 RenameTextContributor 組件
在 %Program Files%\Microsoft Visual Studio 10.0\VSTSDB\Extensions 資料夾中建立名為 MyExtensions 的資料夾。
將已簽署的組件 (RenameTextContributor.dll) 複製到 %Program Files%\Microsoft Visual Studio 10.0\VSTSDB\Extensions\MyExtensions 資料夾。
注意事項 建議您不要直接將 XML 檔案複製到 %Program Files%\Microsoft Visual Studio 10.0\VSTSDB\Extensions 資料夾中。 如果改用子資料夾,可以防止不小心變更 Visual Studio 隨附的其他檔案。
接下來,您必須註冊組件 (一種「擴充功能」(Feature Extension) 類型),以便讓它出現在 Visual Studio 中。
若要註冊 RenameTextContributor 組件
按一下 [檢視] 功能表上的 [其他視窗],然後按一下 [命令視窗] 開啟 [命令視窗]。
在 [命令] 視窗中輸入下列程式碼。 將 FilePath 替代為已編譯之 .dll 檔案的路徑和檔案名稱。 請在路徑和檔案名稱周圍加上引號。
注意事項 根據預設,已編譯之 .dll 檔案的路徑為 <您的方案路徑>\bin\Debug 或 <您的方案路徑>\bin\Release。
? System.Reflection.Assembly.LoadFrom("FilePath").FullName
? System.Reflection.Assembly.LoadFrom(@"FilePath").FullName
按 ENTER。
將產生的程式碼行複製到剪貼簿中。 此程式碼行應該與下列程式碼相似:
"RenameTextContributor, Version=1.0.0.0, Culture=neutral, PublicKeyToken=nnnnnnnnnnnnnnnn"
開啟純文字編輯器,如 [記事本]。
提供下列資訊,並指定您自己的組件名稱、公開金鑰語彙基元和副檔名類型:
<?xml version="1.0" encoding="utf-8" ?> <extensions assembly="" version="1" xmlns="urn:Microsoft.Data.Schema.Extensions" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:Microsoft.Data.Schema.Extensions Microsoft.Data.Schema.Extensions.xsd"> <extension type="MySamples.Refactoring.RenameReferenceTextContributor" assembly="RenameTextContributor, Version=1.0.0.0, Culture=neutral, PublicKeyToken=<enter key here>" enabled="true" /> </extensions>
註冊繼承自 RefactoringContributor 的類別。
將此檔案另存為 %Program Files%\Microsoft Visual Studio 10.0\VSTSDB\Extensions\MyExtensions 資料夾中的 RenameTextContributor.extensions.xml。
關閉 Visual Studio。
接下來,您將建立非常簡單的資料庫專案來測試新的重構類型。
測試新的重構參與者
若要建立資料庫專案
在 [檔案] 功能表中,指向 [新增],然後按一下 [專案]。
展開 [已安裝的範本] 底下的 [資料庫] 節點,然後按一下 [SQL Server] 節點。
在範本清單中,按一下 [SQL Server 2008 資料庫專案]。
按一下 [確定] 接受預設專案名稱並建立專案。
空資料庫專案隨即建立。
若要加入具有主索引鍵的資料表
按一下 [檢視] 功能表上的 [資料庫結構描述檢視]。
在 [結構描述檢視] 中依序展開 [結構描述] 節點和 [dbo] 節點,以滑鼠右鍵按一下 [資料表] 節點,指向 [加入],然後按一下 [資料表]。
在 [加入新項目] 對話方塊的 [名稱] 中,輸入 employee。
注意事項 這裡會刻意使用小寫字母做為資料表名稱的開頭。
按一下 [確定]。
展開 [資料表] 節點,以滑鼠右鍵按一下 [employee] 節點,指向 [加入],然後按一下 [主索引鍵]。
在 [加入新項目] 對話方塊的 [名稱] 中,輸入 PK_Employee_column_1。
按一下 [確定]。
接下來,您會將文字檔加入至資料庫專案,其中包含 employee 資料表的參考。
若要加入包含資料表名稱的文字檔
在 [方案總管] 中,以滑鼠右鍵按一下資料庫專案節點,指向 [加入],然後按一下 [新增項目]。
在 [加入新項目] 對話方塊的 [類別] 清單中,按一下 [Visual Studio 樣板]。
在 [範本] 清單中,按一下 [文字檔]。
在 [名稱] 方塊中,輸入 SampleText1.txt。
在程式碼編輯器中加入下列文字:
This is documentation for the employee table. Any changes made to the employee table name should also be reflected in this text file.
按一下 [檔案] 功能表上的 [儲存 SampleText1.txt]。
接下來,您將使用新的重構類型來變更資料表名稱及其所有參考。
若要使用新的重構參與者更新資料表名稱
在 [結構描述檢視] 中,以滑鼠右鍵按一下 [employee] 資料表節點,指向 [重構],然後按一下 [重新命名]。
在 [重新命名] 對話方塊的 [新名稱] 中,輸入 [Person]。
在 [預覽變更] 對話方塊中,捲動變更群組直到看到 [文字檔] 群組。
文字檔中的兩個 employee 執行個體將會列出。 您已定義類別來啟用此逐步解說中的新重構類型。 選取每項變更旁的核取方塊。
按一下 [套用]。
資料表名稱會更新為 Person,包括在 [結構描述檢視] 以及在 SampleText1.txt 檔案的內容中。
後續步驟
您可以建立其他重構目標,或可以建立新的重構類型,以減少重複變更資料庫專案所需的人力。