Saturday, November 22, 2008

Custom String to Resource Refactoring with DXCore

In my ChaosFilter project, I'm factoring all the text and strings into a resource class.  The intent is to allow for customization by the end user for different installations and possibly translation at some point.  My solution is to place all my text into a class with properties that figure out how to get the text based based upon a unique key and context (which company/application they are running in).  Currently these are stored in the database and then loaded into a cached dictionary object.  This appears to work very fast, however with my architecture and a bit of code gen this could easily be replaced by more traditional resources. 

In the past it's been a pain the a** since I needed to create the label, put the marker into my class file then insert the field into the database.  A better solution certainly exists.  My solution is to build a DXCore custom refactoring.  The idea would be if I'm in a C# class within a string literal or an ASP.NET page within some static HTML, I could hit the refacting key and and take that text and convert it into a message or a simple label.  The difference being a label is only a few words where the message could be sentance or two.  For larger blocks of text I have a different approach.

My Solution

For a ASP.NET page: 
image

For a C# code file (in a library assembly or code behind):

Then to enter the text:

So how was this done?  Actually it was fairly trivial, I built part of it early this week in a couple hours, the rest this morning.  Probably < 4 hours for the whole thing and I haven't built a plugin for a couple years.

Getting Started

This link does a much better job than I could to show the basics of creating a DXCore plugin, but unless I missed it, it didn't cover too much about creating custom refactorings.

http://community.devexpress.com/forums/t/68994.aspx

So assuming that you have the frameworks setup for your plugin (I'm going to skip a few steps not relevant and found elsewhere), here's what you need to do:

1) Create a new DXCore Plug for your refactoring:

2) By default the Refactoring Provider that you need to create a custom Refactoring is not in the toolbox, it can be added by "Choose Items" and selecting the RefactoringProvider
image

3) Once you add that, drop that onto your custom design surface for your Refactoring plugin, it should look something boring like:

4) The click on Properties for your component and add appropriate values:

5) Now go in and click on the highlighed events to build up your stubs:

6) Finally go in and add your code (painting code from: http://tinyurl.com/5a3pe2):

private bool _handlingEditorForegroundPaint = false;
private SourceRange _previewRange;
private DevExpress.CodeRush.UserControls.CodePreviewWindow _previewWindow = null;

// DXCore-generated code...

#region InitializePlugIn
#region FinalizePlugIn

private void ConvertToCustomCaption_Apply(object sender, ApplyContentEventArgs ea)
{
var customLabel = new ui.CustomCaption();
   ea.Element.SelectFullBlock();
   customLabel.ShowWithString(CodeRush.Selection.Text, CodeRush.Caret.ScreenPosition);
if (!customLabel.Cancelled)
      ea.Selection.Text = string.Format("Customization.Captions.{0}", customLabel.LabelKey);
}

private void ConvertToCustomCaption_CheckAvailability(object sender, CheckContentAvailabilityEventArgs ea)
{
   ea.Available = CodeRush.Caret.InsideString && ea.Element.InsideClass;
}

private void ConvertToCustomCaption_HidePreview(object sender, HideContentPreviewEventArgs ea)
{
if (_previewWindow != null)
   {
      _previewWindow.HidePreview();
      _previewWindow = null;
   }
   _previewRange = SourceRange.Empty;
if (_handlingEditorForegroundPaint)
   {
      _handlingEditorForegroundPaint = false;
EventNexus.EditorPaintForeground += new EditorPaintEventHandler(EventNexus_EditorPaintForeground);
   }
}

private void ConvertToCustomCaption_PreparePreview(object sender, PrepareContentPreviewEventArgs ea)
{
PrimitiveExpression ele = ea.Element as PrimitiveExpression;
if (ele != null)
   {
      _previewRange = ele.Range.Clone();
EventNexus.EditorPaintForeground += new EditorPaintEventHandler(EventNexus_EditorPaintForeground);
      CreatePreviewWindow(ea, "Customization.Captions.[New]");
   }
}

private void CreatePreviewWindow(PrepareRefactoringPreviewEventArgs ea, string codeToPreview)
{
   _previewWindow = new CodePreviewWindow(ea.TextView, _previewRange.Top);
   _previewWindow.AddCode(codeToPreview);
   _previewWindow.ShowPreview();
}

private void InvalidatePreviews(RefactoringPreviewEventArgs ea)
{
if (_previewRange.IsEmpty)
return;

   int doubleSpaceWidth = ea.TextView.SpaceWidth * 2;
int textViewLineHeight = ea.TextView.LineHeight;
Rectangle previewRect = ea.TextView.GetRectangleFromRange(_previewRange);
   previewRect.Inflate(doubleSpaceWidth, textViewLineHeight);
ea.TextView.Invalidate(previewRect);
}

void EventNexus_EditorPaintForeground(EditorPaintEventArgs ea)
{
if (_previewRange.IsEmpty)
return;

   using (StrikeThrough strikeThrough = new StrikeThrough())
   {
      strikeThrough.TextView = ea.TextView;
      strikeThrough.FillColor = Color.Red;
      strikeThrough.Range = _previewRange;
      strikeThrough.Paint(ea.Graphics);
   }
}

The "thingy" that let's me enter the text is just a simple WinForm represented by ui.CustomCaption, I won't bore you with that implemetentation since it is fairly tightly coupled intergrated into my architecture.

Enjoy!

-ec

No comments:

Post a Comment