UWP: Compute text height in a RichTextBlock gives weird results - c#

I need a reliable method to get the height of the text contained in a RichTextBlock, even before it is actually drawn on the scene.
Using the normal Measure() method produces a weird result, as it can be seen in the MVCE: https://github.com/cghersi/UWPExamples/tree/master/MeasureText (I want to keep fiexed the width, and measure the final height, but the result of DesiredSize is far different from the actual height!!).
For this reason, I found a rough method (mentioned here https://stackoverflow.com/a/45937298/919700), that I extended to serve my purpose, where we use some Win2D API to compute the content height.
The problem is that in some cases, this method provides an height that is smaller than the expected one.
Is there a general way to retrieve the (correct) height of a
TextBlock, even before it is drawn on the scene?
If this is not the case, what am I doing wrong?
Here's my code (which you can find also as MVCE here: https://github.com/cghersi/UWPExamples/tree/master/RichText):
public sealed partial class MainPage
{
public static readonly FontFamily FONT_FAMILY = new FontFamily("Assets/paltn.ttf#Palatino-Roman");
public const int FONT_SIZE = 10;
private readonly Dictionary<string, object> FONT = new Dictionary<string, object>
{
{ AttrString.FONT_FAMILY_KEY, FONT_FAMILY },
{ AttrString.FONT_SIZE_KEY, FONT_SIZE },
{ AttrString.LINE_HEAD_INDENT_KEY, 10 },
{ AttrString.LINE_SPACING_KEY, 1.08 },
{ AttrString.FOREGROUND_COLOR_KEY, new SolidColorBrush(Colors.Black) }
};
// ReSharper disable once PrivateFieldCanBeConvertedToLocalVariable
private readonly RichTextBlock m_displayedText;
public MainPage()
{
InitializeComponent();
// create the text block:
m_displayedText = new RichTextBlock
{
MaxLines = 0, //Let it use as many lines as it wants
TextWrapping = TextWrapping.Wrap,
AllowFocusOnInteraction = false,
IsHitTestVisible = false,
Width = 80,
Height = 30,
Margin = new Thickness(100)
};
// set the content with the right properties:
AttrString content = new AttrString("Excerpt1 InkLink", FONT);
SetRichText(m_displayedText, content);
// add to the main panel:
MainPanel.Children.Add(m_displayedText);
// compute the text height: (this gives the wrong answer!!):
double textH = GetRichTextHeight(content, (float)m_displayedText.Width);
Console.WriteLine("text height: {0}", textH);
}
public static double GetRichTextHeight(AttrString text, float maxWidth)
{
if (text == null)
return 0;
CanvasDevice device = CanvasDevice.GetSharedDevice();
double finalH = 0;
foreach (AttributedToken textToken in text.Tokens)
{
CanvasTextFormat frmt = new CanvasTextFormat()
{
Direction = CanvasTextDirection.LeftToRightThenTopToBottom,
FontFamily = textToken.Get(AttrString.FONT_FAMILY_KEY, FONT_FAMILY).Source,
FontSize = textToken.Get(AttrString.FONT_SIZE_KEY, FONT_SIZE),
WordWrapping = CanvasWordWrapping.Wrap
};
CanvasTextLayout layout = new CanvasTextLayout(device, textToken.Text, frmt, maxWidth, 0f);
finalH += layout.LayoutBounds.Height;
}
return finalH;
//return textBlock.Blocks.Sum(block => block.LineHeight);
}
private static void SetRichText(RichTextBlock label, AttrString str)
{
if ((str == null) || (label == null))
return;
label.Blocks.Clear();
foreach (AttributedToken token in str.Tokens)
{
Paragraph paragraph = new Paragraph()
{
TextAlignment = token.Get(AttrString.TEXT_ALIGN_KEY, TextAlignment.Left),
TextIndent = token.Get(AttrString.LINE_HEAD_INDENT_KEY, 0),
};
double fontSize = token.Get(AttrString.FONT_SIZE_KEY, FONT_SIZE);
double lineSpacing = token.Get(AttrString.LINE_SPACING_KEY, 1.0);
paragraph.LineHeight = fontSize * lineSpacing;
paragraph.LineStackingStrategy = LineStackingStrategy.BlockLineHeight;
Run run = new Run
{
Text = token.Text,
FontFamily = token.Get(AttrString.FONT_FAMILY_KEY, FONT_FAMILY),
FontSize = fontSize,
Foreground = token.Get(AttrString.FOREGROUND_COLOR_KEY, new SolidColorBrush(Colors.Black)),
FontStyle = token.Get(AttrString.ITALIC_KEY, false) ?
Windows.UI.Text.FontStyle.Italic : Windows.UI.Text.FontStyle.Normal
};
paragraph.Inlines.Add(run);
label.Blocks.Add(paragraph);
}
}
}
public class AttrString
{
public const string FONT_FAMILY_KEY = "Fam";
public const string FONT_SIZE_KEY = "Size";
public const string LINE_HEAD_INDENT_KEY = "LhI";
public const string LINE_SPACING_KEY = "LSpace";
public const string FOREGROUND_COLOR_KEY = "Color";
public const string ITALIC_KEY = "Ita";
public const string TEXT_ALIGN_KEY = "Align";
public const string LINE_BREAK_MODE_KEY = "LineBreak";
public static Dictionary<string, object> DefaultCitationFont { get; set; }
public static Dictionary<string, object> DefaultFont { get; set; }
public List<AttributedToken> Tokens { get; set; }
public AttrString(string text, Dictionary<string, object> attributes)
{
Tokens = new List<AttributedToken>();
Append(text, attributes);
}
public AttrString(AttrString copy)
{
if (copy?.Tokens == null)
return;
Tokens = new List<AttributedToken>(copy.Tokens);
}
public AttrString Append(string text, Dictionary<string, object> attributes)
{
Tokens.Add(new AttributedToken(text, attributes));
return this;
}
public bool IsEmpty()
{
foreach (AttributedToken t in Tokens)
{
if (!string.IsNullOrEmpty(t.Text))
return false;
}
return true;
}
public override string ToString()
{
StringBuilder sb = new StringBuilder();
foreach (AttributedToken t in Tokens)
{
sb.Append(t.Text);
}
return sb.ToString();
}
}
public class AttributedToken
{
public string Text { get; set; }
public Dictionary<string, object> Attributes { get; set; }
public AttributedToken(string text, Dictionary<string, object> attributes)
{
Text = text;
Attributes = attributes;
}
public T Get<T>(string key, T defaultValue)
{
if (string.IsNullOrEmpty(key) || (Attributes == null))
return defaultValue;
if (Attributes.ContainsKey(key))
return (T)Attributes[key];
else
return defaultValue;
}
public override string ToString()
{
return Text;
}
}
** UPDATE **:
After further digging into the issue, the problem seems related to the lack of configurability for the CanvasTextFormat object, especially for the indentation of the first line (expressed in the RichTextBlock using the property Paragraph.TextIndent). Is there any way to specify such setting in a CanvasTextFormat object?

Looking at your MeasureText MVCE code, the problem with calling Measure() on the RichTextBlock comes down to this line:
m_textBlock.Margin = new Thickness(200);
This sets a universal margin of 200 on all sides, which means the element needs at least 200 width on the left plus 200 width on the right, or 400 width. Since your Measure(300,infinite) specifies an available width of less than the minimum required 400 width, the RichTextBlock decides that the best it can do is wrap the text at every character, producing the massive 5740 pixel height (plus the 200+200 height from the margin).
If you remove that line, the RichTextBlock will use the specified constraint of 300 and correctly measure its desired height as 90 pixels, which is what it renders as on screen (if you set Width=300 or otherwise result in the actual element layout to have the same constraint).
Alternatively, since you know the width you want for the element, you could set Width=300 on it and it will then measure with that width. The Height will be expanded as a result of the set Margin, though.
I'm assuming you don't actually have Margin=200 set in your real app, and instead have something smaller like Margin=5 to account for margin you actually want when the RichTextBlock is in the tree and drawing. If this is the case, then you can either:
Use the Width=300 approach for measuring and subtract off the top+bottom margin from the DesireSize.Height.
Measure with (300+margin.Left+margin.Right) as the width so that once the margin is subtracted off from that total availableSize the remaining width the text can use is your intended 300. You'll still need to subtract off the top+bottom margin from the DesireSize.Height.

Related

Concatenating two arrays results in two modified arrays instead of one C#

I have a very strange Behavior when merging two arrays together:
Assumptions
I have a class Tensor which contains an array float[] and a function AddTensorElements:
class Tensor
{
public float[] MovingAverage3h { get; set; }
public float[] MovingAverage6h { get; set; }
public float[] MovingAverage1d { get; set; }
public void AddTensorElements(Tensor input)
{
if (this.MovingAverage3h == null)
this.MovingAverage3h = input.MovingAverage3h;
this.MovingAverage6h = input.MovingAverage6h;
this.MovingAverage1d = input.MovingAverage1d;
}
else
{
this.MovingAverage3h = Concat(this.MovingAverage3h, input.MovingAverage6h);
this.MovingAverage6h = Concat(this.MovingAverage6h, input.MovingAverage6h);
this.MovingAverage1d = Concat(this.MovingAverage1d, input.MovingAverage1d);
}
private float[] Concat (float[] first, float[] second)
{
List<float> concatenated = new List<float>();
concatenated.AddRange(first);
concatenated.AddRange(second);
//foreach (float value in first) concatenated.Add(value);
//foreach (float value in second) concatenated.Add(value);
float[] returnArray = concatenated.ToArray();
return returnArray;
}
}
Within my main program, I repeatedly add the tensor M6t to the base tensor Minutes30[i]
class TensorCreator
{
private static List<Elements.Tensor> Minutes30 = new List<Elements.Tensor>();
private static void AddValues(Tensor M6t)
{
// Fill Minutes 30
Minutes30.Add(M6t);
for (int i = CounterM30; i < Minutes30.Count-1; i += M6)
{
{ } // Issue come up right here
Minutes30[i].AddTensorElements(M6t);
{ } // Issue come up right here
}
}
public static void AppendDataToTensor(Elements.Tensor queueElement)
{
// ...
AddValues(M6Avg);
}
}
Expected behavior vs actual behavior
The array within Minutes[i] expands
The array within M6t staysfloat[1]
So far so good, this works in a tiny separate test application
Within my actual application, the same code lets the baseTensor expand but also the input tensor gets expanded!
for (int i = CounterM30; i < Minutes30.Count-1; i += M6)
{
// M6T.Length == 1;
Minutes30[i].AddTensorElements(M6t);
// M6T.Length == Minutes30[i].Length;
}
strangely, whitin AddtensorToElements() I can see the values changing as well:
The Issue lies right here:
Minutes30.Add(M6t);
This adds a reference of Class Tensor M6t to Minutes 30. The result is that Minutes30[i] gets concatenated with it self.
Solution:
In class Tensor, add a Clone() method
public Tensor Clone()
{
Tensor tensor = new Tensor();
tensor.MovingAverage3h = this.MovingAverage3h.ToArray();
tensor.MovingAverage6h = this.MovingAverage6h.ToArray();
tensor.MovingAverage1d = this.MovingAverage1d.ToArray();
return tensor;
}
then change
Minutes30.Add(M6t);
to
Minutes30.Add(M6t.Clone());

Compare user's input against prefabricated text chain

I am in my first steps towards creating a very basic structural analysis software using Visual C#.
I decided to make it console-based (no user interface). Therefore the only way to get user's input is through chars and strings.
Imagine the user wants to create a 2D bar element. She would need to specify an initial point, a final point and a name for that bar. I want the syntax to be like follows:
"CREATE bar NAMED (bar_name) FIRST (first_point) LAST (last_point)"
Where:
(bar_name) is the name of the bar, up to the user. Let (object_name)="bar_A" (string type).
(first_point) would be the initial point of the bar. Since we are creating a 2D bar, (first_point) should be a 1x2 vector that the user should enter between parenthesis. For example, (first_point)=(0,0)
(last_point) would be the final point of the bar. Same type and syntax as (first_point).
I am just wondering if there is any easy way to achieve the string comparison task, something like comparing the user's input against a prefabricated command.
Of course without forgetting about user's input cleaning task.
I know there is a huge amount of possible solutions here. Maybe using LINQ. Maybe just using the String object. I just want to know the most efficient way, where efficient means:
The fastest the user's query gets processed, the better;
the less the lines of codes, the better; and
where thorough query sanitizing tasks are made.
This last point is really important since some user's input like this:
"CREATE bar NAMED bar_a FISRT (0,0) LAST (0,1)"
Note that the user commited a typo (FISRT instead of FIRST), and the query shouldn't run.
Thanks
Okay, I created a simple parser that should work good for you and, if the need arises, you can easily expand.
Start off by creating a new Console Application. Add a new class file called Tokenizer.cs. This file was auto generated by my TokenIcer project that I linked to you in the comments above. Make Tokenizer.cs look like this:
public class TokenParser
{
private readonly Dictionary<Tokens, string> _tokens;
private readonly Dictionary<Tokens, MatchCollection> _regExMatchCollection;
private string _inputString;
private int _index;
public enum Tokens
{
UNDEFINED = 0,
CREATE = 1,
FIRST = 2,
LAST = 3,
BAR = 4,
NAMED = 5,
BAR_NAME = 6,
WHITESPACE = 7,
LPAREN = 8,
RPAREN = 9,
COMMA = 10,
NUMBER = 11
}
public string InputString
{
set
{
_inputString = value;
PrepareRegex();
}
}
public TokenParser()
{
_tokens = new Dictionary<Tokens, string>();
_regExMatchCollection = new Dictionary<Tokens, MatchCollection>();
_index = 0;
_inputString = string.Empty;
_tokens.Add(Tokens.CREATE, "[Cc][Rr][Ee][Aa][Tt][Ee]");
_tokens.Add(Tokens.FIRST, "[Ff][Ii][Rr][Ss][Tt]");
_tokens.Add(Tokens.LAST, "[Ll][Aa][Ss][Tt]");
_tokens.Add(Tokens.BAR, "[Bb][Aa][Rr][ \\t]");
_tokens.Add(Tokens.NAMED, "[Nn][Aa][Mm][Ee][Dd]");
_tokens.Add(Tokens.BAR_NAME, "[A-Za-z_][a-zA-Z0-9_]*");
_tokens.Add(Tokens.WHITESPACE, "[ \\t]+");
_tokens.Add(Tokens.LPAREN, "\\(");
_tokens.Add(Tokens.RPAREN, "\\)");
_tokens.Add(Tokens.COMMA, "\\,");
_tokens.Add(Tokens.NUMBER, "[0-9]+");
}
private void PrepareRegex()
{
_regExMatchCollection.Clear();
foreach (KeyValuePair<Tokens, string> pair in _tokens)
{
_regExMatchCollection.Add(pair.Key, Regex.Matches(_inputString, pair.Value));
}
}
public void ResetParser()
{
_index = 0;
_inputString = string.Empty;
_regExMatchCollection.Clear();
}
public Token GetToken()
{
if (_index >= _inputString.Length)
return null;
foreach (KeyValuePair<Tokens, MatchCollection> pair in _regExMatchCollection)
{
foreach (Match match in pair.Value)
{
if (match.Index == _index)
{
_index += match.Length;
return new Token(pair.Key, match.Value);
}
if (match.Index > _index)
{
break;
}
}
}
_index++;
return new Token(Tokens.UNDEFINED, string.Empty);
}
public PeekToken Peek()
{
return Peek(new PeekToken(_index, new Token(Tokens.UNDEFINED, string.Empty)));
}
public PeekToken Peek(PeekToken peekToken)
{
int oldIndex = _index;
_index = peekToken.TokenIndex;
if (_index >= _inputString.Length)
{
_index = oldIndex;
return null;
}
foreach (KeyValuePair<Tokens, string> pair in _tokens)
{
var r = new Regex(pair.Value);
Match m = r.Match(_inputString, _index);
if (m.Success && m.Index == _index)
{
_index += m.Length;
var pt = new PeekToken(_index, new Token(pair.Key, m.Value));
_index = oldIndex;
return pt;
}
}
var pt2 = new PeekToken(_index + 1, new Token(Tokens.UNDEFINED, string.Empty));
_index = oldIndex;
return pt2;
}
}
public class PeekToken
{
public int TokenIndex { get; set; }
public Token TokenPeek { get; set; }
public PeekToken(int index, Token value)
{
TokenIndex = index;
TokenPeek = value;
}
}
public class Token
{
public TokenParser.Tokens TokenName { get; set; }
public string TokenValue { get; set; }
public Token(TokenParser.Tokens name, string value)
{
TokenName = name;
TokenValue = value;
}
}
In Program.cs, make it look like this:
class Program
{
private class Bar
{
public string Name { get; set; }
public int FirstX { get; set; }
public int FirstY { get; set; }
public int LastX { get; set; }
public int LastY { get; set; }
}
static void Main(string[] args)
{
const string commandCreateBar1 = "CREATE bar NAMED bar_a FIRST(5,10) LAST (15,20)";
const string commandCreateBar2 = "CREATE bar NAMED MyFooBar FIRST(25 , 31) LAST (153 ,210)";
const string commandCreateBar3 = "CREATE bar NAMED MySpaceyFooBar FIRST(0,0) LAST (12,39)";
Bar bar1 = ParseCreateBar(commandCreateBar1);
PrintBar(bar1);
Bar bar2 = ParseCreateBar(commandCreateBar2);
PrintBar(bar2);
Bar bar3 = ParseCreateBar(commandCreateBar3);
PrintBar(bar3);
}
private static void PrintBar(Bar bar)
{
Console.WriteLine("A new bar was Created! \"{0}\" ({1}, {2}) ({3}, {4})", bar.Name, bar.FirstX, bar.FirstY, bar.LastX, bar.LastY);
}
private static Bar ParseCreateBar(string commandLine)
{
var bar = new Bar();
var parser = new TokenParser { InputString = commandLine };
Expect(parser, TokenParser.Tokens.CREATE);
Expect(parser, TokenParser.Tokens.BAR);
Expect(parser, TokenParser.Tokens.NAMED);
Token token = Expect(parser, TokenParser.Tokens.BAR_NAME);
bar.Name = token.TokenValue;
Expect(parser, TokenParser.Tokens.FIRST);
Expect(parser, TokenParser.Tokens.LPAREN);
token = Expect(parser, TokenParser.Tokens.NUMBER);
bar.FirstX = int.Parse(token.TokenValue);
Expect(parser, TokenParser.Tokens.COMMA);
token = Expect(parser, TokenParser.Tokens.NUMBER);
bar.FirstY = int.Parse(token.TokenValue);
Expect(parser, TokenParser.Tokens.RPAREN);
Expect(parser, TokenParser.Tokens.LAST);
Expect(parser, TokenParser.Tokens.LPAREN);
token = Expect(parser, TokenParser.Tokens.NUMBER);
bar.LastX = int.Parse(token.TokenValue);
Expect(parser, TokenParser.Tokens.COMMA);
token = Expect(parser, TokenParser.Tokens.NUMBER);
bar.LastY = int.Parse(token.TokenValue);
Expect(parser, TokenParser.Tokens.RPAREN);
return bar;
}
private static Token Expect(TokenParser parser, TokenParser.Tokens expectedToken)
{
EatWhiteSpace(parser);
Token token = parser.GetToken();
if (token != null && token.TokenName != expectedToken)
{
Console.WriteLine("Expected Token " + expectedToken);
Environment.Exit(0);
}
if (token == null)
{
Console.WriteLine("Unexpected end of input!");
Environment.Exit(0);
}
return token;
}
private static void EatWhiteSpace(TokenParser parser)
{
while (parser.Peek() != null && parser.Peek().TokenPeek != null &&
parser.Peek().TokenPeek.TokenName == TokenParser.Tokens.WHITESPACE)
{
parser.GetToken();
}
}
}
As you can see, I created 3 test scenarios. Notice all white space is ignored. If you want to be strict about the white space, you can modify the EatWhiteSpace function to be strict.
If you want, I have a simple expression parser I could throw into this code too, that way you could have commands such as CREATE bar NAMED bar_a FIRST(3+2, 7*8 + 12) LAST (150-100, 12-3*2). I've got a simple expression parser I made a while back using TokenIcer that I can throw in. It can parse any math expression and supports parenthesis, add, subtract, multiply, and divide.
Tokenization is one way to go, but if you aren't planning on supporting way too many commands and parameters, you should look at Regexes.
Regex regex = new Regex(#"^CREATE bar NAMED (?<BarName>[A-Za-z0-9-_]*) FIRST (?<FirstPoint>\([0-9]+\|[0-9]+\)) LAST (?<LastPoint>\([0-9]+\|[0-9]+\)$");
Match match = regex.Match("create bar named bar_a first (0,0) last (0,1)", RegexOptions.IgnoreCase);
if (match.Success)
{
var name = match.Groups["BarName"].Value;
// and so on for other matches
}

UITextField PlaceHolder Color change Monotouch

I have been trying to change the color of UItextfield placeholder and I have had no luck. I have tried sub classing, and I have tried modifying the Attributed string but nothing worked.
This is the subclass I tried.
public class CustomTextbox : UITextField
{
private string placeholder{ get; set; }
public CustomTextbox ()
{
}
public CustomTextbox(string theString)
{
this.placeholder = theString;
}
public override void DrawPlaceholder (RectangleF rect) {
using (UIFont font = UIFont.SystemFontOfSize (16))
using (UIColor col = new UIColor (UIColor.Blue.CGColor)) {
col.SetFill (); col.SetStroke (); col.SetColor (); base.DrawString (base.Placeholder,rect, font); } }
}
This is the attributed string method I tried:
var textat = new NSMutableAttributedString ("Email", new CTStringAttributes () {
ForegroundColor = UIColor.White.CGColor,StrokeColor = UIColor.White.CGColor,
StrokeWidth = 5.0f
});
this.emailtextbox.AttributedPlaceholder = textat;
Just simplify what you already have.
This works on iOS7:
var t = new UITextField()
{
AttributedPlaceholder = new NSAttributedString("some placeholder text", null, UIColor.Red)
}
The monotuch way to do this is (the answer is found here)
myLogin.AttributedPlaceholder = new NSAttributedString (
"Enter your credentials",
font: UIFont.FromName ("HoeflerText-Regular", 24.0f),
foregroundColor: UIColor.Red,
strokeWidth: 4
);
Try this one:
[yourtextField setValue:[UIColor blueColor] forKeyPath:#"_placeholderLabel.textColor"];

Arrays and measurement unit conversion

using (read = new StreamReader("C:/Users/Sam Smith/Desktop/convert.txt"))
{
while (!read.EndOfStream)
{
lineFromFile = read.ReadLine();
units = lineFromFile.Split(',');
if (units.Contains(splitEntry[0]) && units.Contains(splitEntry[1]))
{
firstUnit = units[0];
secondUnit = units[1];
userConvertValue = Convert.ToDouble(splitEntry[2]);
fileConvertValue = Convert.ToDouble(units[2]);
result = fileConvertValue * userConvertValue;
}
if (units.Contains(splitEntry[0]) && units.Contains(splitEntry[1]))
{
firstUnit = units[1];
secondUnit = units[0];
userConvertValue = Convert.ToDouble(splitEntry[2]);
fileConvertValue = Convert.ToDouble(units[2]);
result = userConvertValue / fileConvertValue;
}
if (!units.Contains(splitEntry[0]) || !units.Contains(splitEntry[1]))
{
Console.WriteLine("Error, measurement unit not recognised.");
}
Above I have a text file that contains types of unit measurement (pounds, ounces, miles and such), the text from this file is split into a string array.
The user enters two measurement units in the following format to convert to two units:
unit,unit,amount
In the text file, the conversion amount for two units is every third split string, like so:
unit,unit,2.80
unit,unit,1.27 (etc)
Is there a way of grouping each set of units and their conversion amounts? For example, if the user tries to convert two particular units, the program knows which conversion value to use when calculating the final result.
Might be a little vague, but it's difficult to explain.
EDIT: The user does not interact with the file, the program simply pulls the data from the file, which is then split into strings (',') and stored in an array.
If I don't got you wrong, the following code should fulfill your requirements (it's very basic, no error handling etc.):
public enum Unit
{
Pound,
Kilo,
Kilometer,
Mile
}
public class UnitMapping
{
public UnitMapping(Unit source, Unit target, double factor)
{
SourceUnit = source;
TargetUnit = target;
Factor = factor;
}
public Unit SourceUnit { get; private set; }
public Unit TargetUnit { get; private set; }
public double Factor { get; private set; }
}
public class UnitCalculator
{
public const string FILE_INPUT = #"Kilo,Pound,0.45359237
Kilometer,Mile,1.609344";
private List<UnitMapping> mappings;
public UnitCalculator()
{
this.mappings = new List<UnitMapping>();
// parse the mappings
foreach (var line in FILE_INPUT.Split(Environment.NewLine.ToCharArray(), StringSplitOptions.RemoveEmptyEntries))
{
var fields = line.Split(',');
var source = (Unit)Enum.Parse(typeof(Unit), fields[0]);
var target = (Unit)Enum.Parse(typeof(Unit), fields[1]);
double factor = double.Parse(fields[2], CultureInfo.InvariantCulture);
this.mappings.Add(new UnitMapping(source, target, factor));
}
}
public double Convert(Unit source, Unit target, double value)
{
foreach (var mapping in this.mappings)
{
if (mapping.SourceUnit == source && mapping.TargetUnit == target)
{
return value * mapping.Factor;
}
else if (mapping.SourceUnit == target && mapping.TargetUnit == source)
{
return value * (1 / mapping.Factor);
}
}
throw new InvalidOperationException("No mapping could be found for this conversion.");
}
}
Invoke it like this:
static void Main(string[] args)
{
var calc = new UnitCalculator();
Console.WriteLine(calc.Convert(Unit.Mile, Unit.Kilometer, 1));
}
If you don't know the units, you can use strings as well.

How to create a Xaml FlowDocument with Page Headers and Footers when rendered to XPS?

I am looking for an idea of a clean generic way to describe repeating page headers and footers in a XAML FlowDocument without any code behind. It only needs to show up correctly when rendered to XPS from C#.
I had the same problem a few months ago, and found these links very helpful:
WPF Multipage Reports Part IV Pagination
http://www.codeproject.com/KB/WPF/PimpedDocumentPaginator.aspx
The basic technique I used was to create a custom paginator by deriving from DynamicDocumentPaginator as follows:
internal class HeaderFooterPaginator<THeaderModel, TFooterModel> : DynamicDocumentPaginator where THeaderModel : PageNumberModel, new() where TFooterModel : PageNumberModel, new()
{
...
}
In my case, THeaderFooterModel and TFooterModel are subclasses of a PageNumberModel type as I needed the header or footer to be able to show the current page number.
public class PageNumberModel
{
public int PageNumber { get; set; }
}
The custom paginator delegates to the original XPS paginator to do the majority of its work, so it stores it away in the constructor.
The THeaderModel and TFooterModel types allow the paginator to retrieve XAML DataTemplates for each type, which is what allows you to specify the layout of the header and footer in XAML without resorting to custom drawing code.
In my code, the header and footer are of a fixed size, so when the paginator is created it retrieves the header and footer templates to determine how much space to reserve.
In the example code in the links provided, the technique they use to reserve space for the header and footer is to use a scale transform to shrink the original content. Instead, I tell the original paginator to use a reduced page size and then add the page the original paginator generates to a ContainerVisual and set its Offset. You probably couldn't do this if the size of the headers and footers was dynamic because the page count would keep changing.
The only other complication I can recall was that you need to use the Dispatcher queue when adding headers and footers (see AddHeaderOrFooterToContainerAsync below). Data binding doesn't work otherwise. We are slightly subverting the WPF rendering model to get this to work.
This would all be quite hard to explain without including the code so I've attached the custom renderer code below. I've stripped out some irrelevant stuff so if it doesn't compile as is that's probably why :-)
Note that the page number offset is passed in because our XPS document is comprised of multiple FlowDocument sections and the calling code keeps track of the current overall page number.
Hope this helps!
internal class HeaderFooterPaginator<THeaderModel, TFooterModel> : DynamicDocumentPaginator where THeaderModel : PageNumberModel, new() where TFooterModel : PageNumberModel, new()
{
private readonly double _footerHeight;
private readonly DataTemplate _footerTemplate;
private readonly double _headerHeight;
private readonly DataTemplate _headerTemplate;
private Size _newPageSize;
private Size _originalPageSize;
private readonly DynamicDocumentPaginator _originalPaginator;
private readonly int _pageNumberOffset;
private readonly FlowDocument _paginatorSource;
private const double HeaderAndFooterMarginHeight = 5;
private const double HeaderAndFooterMarginWidth = 10;
public HeaderFooterPaginator(int pageNumberOffset, FlowDocument document)
{
if (document == null)
{
throw new ArgumentNullException("document");
}
_paginatorSource = document;
if (_paginatorSource == null)
{
throw new InvalidOperationException("Could not create a clone of the document being paginated.");
}
_originalPaginator = ((IDocumentPaginatorSource) _paginatorSource).DocumentPaginator as DynamicDocumentPaginator;
if (_originalPaginator == null)
{
throw new InvalidOperationException("The paginator must be a DynamicDocumentPaginator.");
}
_headerTemplate = GetModelDataTemplate(typeof (THeaderModel));
_footerTemplate = GetModelDataTemplate(typeof (TFooterModel));
var headerSize = GetModelContentSize(new THeaderModel { PageNumber = int.MaxValue }, _headerTemplate, _originalPaginator.PageSize);
var footerSize = GetModelContentSize(new TFooterModel { PageNumber = int.MaxValue }, _footerTemplate, _originalPaginator.PageSize);
_headerHeight = double.IsInfinity(headerSize.Height) ? 0 : headerSize.Height;
_footerHeight = double.IsInfinity(footerSize.Height) ? 0 : footerSize.Height;
_pageNumberOffset = pageNumberOffset;
SetPageSize(new Size(document.PageWidth, document.PageHeight));
}
private void AddHeaderOrFooterToContainerAsync<THeaderOrFooter>(ContainerVisual container, double areaWidth, double areaHeight, Vector areaOffset, FrameworkTemplate template, int displayPageNumber) where THeaderOrFooter : PageNumberModel, new()
{
if (template == null)
{
return;
}
var visual = GetModelContent(new THeaderOrFooter { PageNumber = displayPageNumber }, template);
if (visual != null)
{
Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.ApplicationIdle, new Action(() =>
{
visual.Measure(_originalPageSize);
visual.Arrange(new Rect(0, 0, areaWidth, areaHeight));
visual.UpdateLayout();
var headerContainer = new ContainerVisual { Offset = areaOffset };
headerContainer.Children.Add(visual);
container.Children.Add(headerContainer);
}));
}
}
public override void ComputePageCount()
{
_originalPaginator.ComputePageCount();
}
private static void FlushDispatcher()
{
Dispatcher.CurrentDispatcher.Invoke(DispatcherPriority.ApplicationIdle, new DispatcherOperationCallback(delegate { return null; }), null);
}
private static FrameworkElement GetModelContent(object dataModel, FrameworkTemplate modelTemplate)
{
if (modelTemplate == null)
{
return null;
}
var content = modelTemplate.LoadContent() as FrameworkElement;
if (content == null)
{
return null;
}
content.DataContext = dataModel;
return content;
}
private static Size GetModelContentSize(object dataModel, FrameworkTemplate modelTemplate, Size availableSize)
{
var content = GetModelContent(dataModel, modelTemplate);
if (content == null)
{
return Size.Empty;
}
FlushDispatcher();
content.Measure(availableSize);
return content.DesiredSize;
}
private DataTemplate GetModelDataTemplate(Type modelType)
{
var key = new DataTemplateKey(modelType);
return _paginatorSource.TryFindResource(key) as DataTemplate;
}
public override ContentPosition GetObjectPosition(object value)
{
return _originalPaginator.GetObjectPosition(value);
}
public override DocumentPage GetPage(int pageNumber)
{
if (!_originalPaginator.IsPageCountValid)
{
ComputePageCount();
}
var originalPage = _originalPaginator.GetPage(pageNumber);
var newPage = new ContainerVisual();
var displayPageNumber = _pageNumberOffset + pageNumber;
var internalWidth = _originalPageSize.Width - 2*HeaderAndFooterMarginWidth;
AddHeaderOrFooterToContainerAsync<THeaderModel>(newPage, internalWidth, _headerHeight, new Vector(HeaderAndFooterMarginWidth, HeaderAndFooterMarginHeight), _headerTemplate, displayPageNumber);
var smallerPage = new ContainerVisual();
smallerPage.Children.Add(originalPage.Visual);
smallerPage.Offset = new Vector(HeaderAndFooterMarginWidth, HeaderAndFooterMarginHeight + _headerHeight);
newPage.Children.Add(smallerPage);
AddHeaderOrFooterToContainerAsync<TFooterModel>(newPage, internalWidth, _footerHeight, new Vector(HeaderAndFooterMarginWidth, _originalPageSize.Height - HeaderAndFooterMarginHeight - _footerHeight), _footerTemplate, displayPageNumber);
return new DocumentPage(newPage, _originalPageSize, originalPage.BleedBox, originalPage.ContentBox);
}
public override int GetPageNumber(ContentPosition contentPosition)
{
return _originalPaginator.GetPageNumber(contentPosition);
}
public override ContentPosition GetPagePosition(DocumentPage page)
{
return _originalPaginator.GetPagePosition(page);
}
private void SetPageSize(Size pageSize)
{
_originalPageSize = pageSize;
// Decrease the available page size by the height of the header and footer. The page is offset by the header height later on.
var sizeRect = new Rect(pageSize);
sizeRect.Inflate(-HeaderAndFooterMarginWidth, -(HeaderAndFooterMarginHeight + _footerHeight/2 + _headerHeight/2));
_originalPaginator.PageSize = _newPageSize = sizeRect.Size;
// Change page size of the document to the size of the content area
_paginatorSource.PageHeight = _newPageSize.Height;
_paginatorSource.PageWidth = _newPageSize.Width;
}
public override bool IsPageCountValid
{
get { return _originalPaginator.IsPageCountValid; }
}
public override int PageCount
{
get { return _originalPaginator.PageCount; }
}
public override Size PageSize
{
get { return _newPageSize; }
set { SetPageSize(value); }
}
public override IDocumentPaginatorSource Source
{
get { return _originalPaginator.Source; }
}
}

Categories

Resources