In my last post, I talked about the enhancements and new architecture I have put into place for the new version
of MoXAML Power Toys. I also mentioned that I’d talk about adding a new command.
Well, in this post I’m going to cover how I coded the Scrubber command that’s available in the new version.
One of the first things I did was break the Scrubber options from the actual Scrubber command. By doing this, the user no longer needs to set the options every time
they need to run Scrubber. So, I created a traditional model for the options which could be used by the Scrubber commands and the Scrubber options dialog. This model looks like this:
using System.ComponentModel;
using System.IO;
using System.IO.IsolatedStorage;
using System.Linq;
namespace MoXAML.Scrubber.Model
{
public class ScrubberOptionsModel : INotifyPropertyChanged
{
private const string FILENAME = "MoXAMLSettings.dat";
private int _attributeCountTolerance = 3;
private bool _reorderAttributes = true;
private bool _reducePrecision = true;
private int _precision = 3;
private bool _removeCommonDefaults = true;
private bool _forceLineMinimum = true;
private int _spaceCount = 2;
private bool _convertTabsToSpaces = true;
public ScrubberOptionsModel()
{
LoadModel();
}
public bool ConvertTabsToSpaces
{
get
{
return _convertTabsToSpaces;
}
set
{
if (_convertTabsToSpaces == value) return;
_convertTabsToSpaces = value;
OnChanged("ConvertTabsToSpaces");
}
}
public int SpaceCount
{
get
{
return _spaceCount;
}
set
{
if (_spaceCount == value) return;
_spaceCount = value;
OnChanged("SpaceCount");
}
}
public bool ForceLineMinimum
{
get
{
return _forceLineMinimum;
}
set
{
if (_forceLineMinimum == value) return;
_forceLineMinimum = value;
OnChanged("ForceLineMinimum");
}
}
public bool RemoveCommonDefaults
{
get
{
return _removeCommonDefaults;
}
set
{
if (_removeCommonDefaults == value) return;
_removeCommonDefaults = value;
OnChanged("RemoveCommonDefaults");
}
}
public int Precision
{
get
{
return _precision;
}
set
{
if (_precision == value) return;
_precision = value;
OnChanged("Precision");
}
}
public bool ReducePrecision
{
get
{
return _reducePrecision;
}
set
{
if (_reducePrecision == value) return;
_reducePrecision = value;
OnChanged("ReducePrecision");
}
}
public bool ReorderAttributes
{
get
{
return _reorderAttributes;
}
set
{
if (_reorderAttributes == value) return;
_reorderAttributes = value;
OnChanged("ReorderAttributes");
}
}
public int AttributeCountTolerance
{
get { return _attributeCountTolerance; }
set
{
if (_attributeCountTolerance == value) return;
_attributeCountTolerance = value;
OnChanged("AttributeCountTolerance");
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnChanged(string propertyName)
{
var handler = PropertyChanged;
if (handler == null) return;
handler(this, new PropertyChangedEventArgs(propertyName));
}
private void LoadModel()
{
using (IsolatedStorageFile file = IsolatedStorageFile.GetUserStoreForAssembly())
{
if (file.GetFileNames(FILENAME).Count() == 0) return;
using (IsolatedStorageFileStream stream =
new IsolatedStorageFileStream(FILENAME, System.IO.FileMode.Open, file))
{
using (StreamReader sr = new StreamReader(stream))
{
_attributeCountTolerance = ConvertInt(sr.ReadLine());
_reorderAttributes = ConvertBool(sr.ReadLine());
_reducePrecision = ConvertBool(sr.ReadLine());
_precision = ConvertInt(sr.ReadLine());
_removeCommonDefaults = ConvertBool(sr.ReadLine());
_forceLineMinimum = ConvertBool(sr.ReadLine());
_spaceCount = ConvertInt(sr.ReadLine());
_convertTabsToSpaces = ConvertBool(sr.ReadLine());
}
}
}
}
public void SaveModel()
{
using (IsolatedStorageFile file = IsolatedStorageFile.GetUserStoreForAssembly())
{
using (IsolatedStorageFileStream stream =
new IsolatedStorageFileStream(FILENAME, System.IO.FileMode.Create, file))
{
using (StreamWriter sr = new StreamWriter(stream))
{
sr.WriteLine(_attributeCountTolerance);
sr.WriteLine(_reorderAttributes);
sr.WriteLine(_reducePrecision);
sr.WriteLine(_precision);
sr.WriteLine(_removeCommonDefaults);
sr.WriteLine(_forceLineMinimum);
sr.WriteLine(_spaceCount);
sr.WriteLine(_convertTabsToSpaces);
}
}
}
}
private int ConvertInt(string value)
{
int retVal = 0;
if (int.TryParse(value, out retVal))
{
return retVal;
}
return 0;
}
private bool ConvertBool(string value)
{
bool retVal = false;
if (bool.TryParse(value, out retVal))
{
return retVal;
}
return false;
}
}
}
As you can see, there’s nothing remarkable about it. It’s a straightforward model implementation, and there’s nothing special about using this inside MoXAML (the point I’m making
here is that you can mix and match your MoXAML implementation with standard .NET code).
OK Pete, let’s have a look at actually hooking this into MoXAML. Well, as I mentioned in the MoXAML page, I wanted to have two versions of Scrubber, one for the current file
and one for the project files. In order to do this, it made sense to pull the core Scrubber functionality into a base class which the actual commands would hook into:
using MoXAML.Infrastructure;
using MoXAML.Scrubber.Model;
using System.IO;
using System.Xml;
using System.Collections.Generic;
using System;
namespace MoXAML.Scrubber
{
public partial class ScrubberCommandBase : CommandBase
{
private ScrubberOptionsModel _model;
public ScrubberCommandBase() : base()
{
_model = new ScrubberOptionsModel();
}
protected void ParseFile(string file)
{
string text = File.ReadAllText(file);
text = Perform(text);
File.WriteAllText(file, text);
}
private string Perform(string text)
{
text = Indent(text);
return ReducePrecision(text);
}
private string IndentString
{
get
{
if (_model.ConvertTabsToSpaces)
{
string spaces = string.Empty;
spaces = spaces.PadRight(_model.SpaceCount, ' ');
return spaces;
}
else
{
return "\t";
}
}
}
private string ReducePrecision(string s)
{
string old = s;
if (_model.ReducePrecision)
{
int begin = 0;
int end = 0;
while (true)
{
begin = old.IndexOf('.', begin);
if (begin == -1) break;
begin++;
for (int i = 0; i < _model.Precision; i++)
{
if (old[begin] >= '0' && old[begin] <= '9') begin++;
}
end = begin;
while (end < old.Length && old[end] >= '0' && old[end] <= '9') end++;
old = old.Substring(0, begin) + old.Substring(end, old.Length - end);
begin++;
}
}
return old;
}
public string Indent(string s)
{
string result;
s = s.Replace("&", "¬¬");
using (MemoryStream ms = new MemoryStream(s.Length))
{
using (StreamWriter sw = new StreamWriter(ms))
{
sw.Write(s);
sw.Flush();
ms.Seek(0, SeekOrigin.Begin);
using (StreamReader reader = new StreamReader(ms))
{
XmlReaderSettings settings = new XmlReaderSettings();
settings.CheckCharacters = false;
settings.ConformanceLevel = ConformanceLevel.Auto;
XmlTextReader xmlReader = new XmlTextReader(reader.BaseStream);
xmlReader.Normalization = false;
xmlReader.Read();
xmlReader.Normalization = false;
string str = "";
while (!xmlReader.EOF)
{
string xml;
int num;
int num6;
int num7;
int num8;
switch (xmlReader.NodeType)
{
case XmlNodeType.Element:
xml = "";
num = 0;
goto Element;
case XmlNodeType.Text:
{
string str4 =
xmlReader.Value.Replace(
"&", "&").Replace(
"<", "<").Replace(
">", ">").Replace(
"\"", """);
str = str + str4;
xmlReader.Read();
continue;
}
case XmlNodeType.ProcessingInstruction:
xml = "";
num7 = 0;
goto ProcessingInstruction;
case XmlNodeType.Comment:
xml = "";
num8 = 0;
goto Comment;
case XmlNodeType.Whitespace:
{
xmlReader.Read();
continue;
}
case XmlNodeType.EndElement:
xml = "";
num6 = 0;
goto EndElement;
default:
goto Other;
}
Label_00C0:
xml = xml + IndentString;
num++;
Element:
if (num < xmlReader.Depth)
{
goto Label_00C0;
}
string elementName = xmlReader.Name;
string str5 = str;
str = str5 + "\r\n" + xml + "<" + xmlReader.Name;
bool isEmptyElement = xmlReader.IsEmptyElement;
if (xmlReader.HasAttributes)
{
List<AttributeValuePair> attributes =
new List<AttributeValuePair>(xmlReader.AttributeCount);
for (int k = 0; k < xmlReader.AttributeCount; k++)
{
xmlReader.MoveToAttribute(k);
string value = xmlReader.Value;
if (_model.RemoveCommonDefaults)
{
if (!AttributeValuePair.IsCommonDefault(
elementName, xmlReader.Name, value))
{
attributes.Add(new AttributeValuePair(
elementName, xmlReader.Name, value));
}
}
else
{
attributes.Add(new AttributeValuePair(
elementName, xmlReader.Name, value));
}
}
if (_model.ReorderAttributes)
{
attributes.Sort();
}
xml = "";
string str3 = "";
int depth = xmlReader.Depth;
for (int j = 0; j < depth; j++)
{
xml = xml + IndentString;
}
foreach (AttributeValuePair a in attributes)
{
string str7 = str;
if (attributes.Count > _model.AttributeCountTolerance
&& !AttributeValuePair.ForceNoLineBreaks(elementName))
{
str = str7 + "\r\n" + xml + str3 +
a.Name + "=\"" + a.Value + "\"";
}
else
{
str = str7 + " " + a.Name + "=\"" + a.Value + "\"";
}
}
}
if (isEmptyElement)
{
str = str + "/";
}
str = str + ">";
xmlReader.Read();
continue;
Label_02F4:
xml = xml + IndentString;
num6++;
EndElement:
if (num6 < xmlReader.Depth)
{
goto Label_02F4;
}
string str8 = str;
str = str8 + "\r\n" + xml + "</" + xmlReader.Name + ">";
xmlReader.Read();
continue;
Label_037A:
xml = xml + " ";
num7++;
ProcessingInstruction:
if (num7 < xmlReader.Depth)
{
goto Label_037A;
}
string str9 = str;
str = str9 + "\r\n" + xml + "<?Mapping " +
xmlReader.Value + " ?>";
xmlReader.Read();
continue;
Comment:
if (num8 < xmlReader.Depth)
{
xml = xml + IndentString;
num8++;
}
str = str + "\r\n" + xml + "<!--" + xmlReader.Value + "-->";
xmlReader.Read();
continue;
Other:
xmlReader.Read();
}
xmlReader.Close();
result = str;
}
}
}
return result.Replace("¬¬", "&");
}
private class AttributeValuePair : IComparable
{
public string Name = "";
public string Value = "";
public AttributeType AttributeType = AttributeType.Other;
public AttributeValuePair(string elementname, string name, string value)
{
Name = name;
Value = value;
if (name.StartsWith("xmlns"))
{
AttributeType = AttributeType.Namespace;
}
else
{
switch (name)
{
case "Key":
case "x:Key":
AttributeType = AttributeType.Key;
break;
case "Name":
case "x:Name":
AttributeType = AttributeType.Name;
break;
case "x:Class":
AttributeType = AttributeType.Class;
break;
case "Canvas.Top":
case "Canvas.Left":
case "Canvas.Bottom":
case "Canvas.Right":
case "Grid.Row":
case "Grid.RowSpan":
case "Grid.Column":
case "Grid.ColumnSpan":
AttributeType = AttributeType.AttachedLayout;
break;
case "Width":
case "Height":
case "MaxWidth":
case "MinWidth":
case "MinHeight":
case "MaxHeight":
AttributeType = AttributeType.CoreLayout;
break;
case "Margin":
case "VerticalAlignment":
case "HorizontalAlignment":
case "Panel.ZIndex":
AttributeType = AttributeType.StandardLayout;
break;
case "mc:Ignorable":
case "d:IsDataSource":
case "d:LayoutOverrides":
case "d:IsStaticText":
AttributeType = AttributeType.BlendGoo;
break;
default:
AttributeType = AttributeType.Other;
break;
}
}
}
#region IComparable Members
public int CompareTo(object obj)
{
AttributeValuePair other = obj as AttributeValuePair;
if (other != null)
{
if (this.AttributeType == other.AttributeType)
{
if (this.Name.Equals("StartPoint") && other.Name.Equals("EndPoint")) return -1;
if (this.Name.Equals("EndPoint") && other.Name.Equals("StartPoint")) return 1;
if (this.Name.Equals("Width") && other.Name.Equals("Height")) return -1;
if (this.Name.Equals("Height") && other.Name.Equals("Width")) return 1;
if (this.Name.Equals("Offset") && other.Name.Equals("Color")) return -1;
if (this.Name.Equals("Color") && other.Name.Equals("Offset")) return 1;
if (this.Name.Equals("TargetName") && other.Name.Equals("Property")) return -1;
if (this.Name.Equals("Property") && other.Name.Equals("TargetName")) return 1;
return Name.CompareTo(other.Name);
}
else
{
return this.AttributeType.CompareTo(other.AttributeType);
}
}
return 0;
}
public static bool IsCommonDefault(string elementname, string name, string value)
{
if (
(name == "HorizontalAlignment" && value == "Stretch") ||
(name == "VerticalAlignment" && value == "Stretch") ||
(name == "Margin" && value == "0") ||
(name == "Margin" && value == "0,0,0,0") ||
(name == "Opacity" && value == "1") ||
(name == "FontWeight" && value == "{x:Null}") ||
(name == "Background" && value == "{x:Null}") ||
(name == "Stroke" && value == "{x:Null}") ||
(name == "Fill" && value == "{x:Null}") ||
(name == "Visibility" && value == "Visible") ||
(name == "Grid.RowSpan" && value == "1") ||
(name == "Grid.ColumnSpan" && value == "1") ||
(name == "BasedOn" && value == "{x:Null}") ||
(elementname != "ColumnDefinition" && elementname !=
"RowDefinition" && name == "Width" && value == "Auto") ||
(elementname != "ColumnDefinition" && elementname !=
"RowDefinition" && name == "Height" && value == "Auto")
)
{
return true;
}
return false;
}
public static bool ForceNoLineBreaks(string elementname)
{
if (
(elementname == "RadialGradientBrush") ||
(elementname == "GradientStop") ||
(elementname == "LinearGradientBrush") ||
(elementname == "ScaleTransfom") ||
(elementname == "SkewTransform") ||
(elementname == "RotateTransform") ||
(elementname == "TranslateTransform") ||
(elementname == "Trigger") ||
(elementname == "Setter")
)
{
return true;
}
else
{
return false;
}
}
#endregion
}
private enum AttributeType
{
Key = 10,
Name = 20,
Class = 30,
Namespace = 40,
CoreLayout = 50,
AttachedLayout = 60,
StandardLayout = 70,
Other = 1000,
BlendGoo = 2000
}
}
}
I appreciate that this is a long listing, but don’t worry about what’s going on inside. To hook into the MoXAML plug-in architecture, only one line in there is really
important: to be picked up.
public partial class ScrubberCommandBase : CommandBase
By inheriting from CommandBase
, MoXAML picks the command up because CommandBase
implements ICommandBase
. The cunning thing about MoXAML is that
it uses MEF which allows you to mark interfaces so that any implementations of the interface will automatically be picked up. Well, we have most of the infrastructure
in place – all we actually need to do is write the actual command. You’ll be surprised how little that actually takes:
using MoXAML.Infrastructure;
namespace MoXAML.Scrubber
{
public class ScrubFileCommand : ScrubberCommandBase
{
public ScrubFileCommand()
: base()
{
CommandName = "ScrubFile";
Caption = "Scrubber";
ParentCommandBar.Add(CommandBarType.XamlContextMenu);
}
public override void Execute()
{
base.Execute();
ParseFile(Application.ActiveDocument.FullName);
}
}
}
Let’s break it down. In the constructor, we’re giving the command a unique name that MoXAML uses to track whether or not the command was previously installed.
The Caption
is what will actually appear in the menu, and the line ParentCommandBar.Add
is used to add the command to the appropriate menu.
Finally, we actually need our command to do something, and that’s where the Execute
method comes in – MoXAML uses this method to execute the command, so it’s the place
to hook our command logic to. In this case, we’re hooking into the ParseFile
method in the class we inherit from.