XamFormsExtended / Xfx.Controls won't allow formatting numbers - c#

I inherited a project that uses the Xamarin Extended Forms controls. All of the text boxes on the form use this. I am trying to do something that feels very simple... I want to display and capture numbers and display them with thousands seperators. So, my odometer field displays 356,098 instead of 356098, much easier to read. It needs to save the integer without formatting in the binding.
I've tried various forms of string formatting on the text box and when my page loads I get an endless loop. It seems like the control formats the field and updates it which triggers the getter/setter which then tries to format it without the comma which triggers the textbox that there is a change, through the binding, and it adds the comma, which triggers another loop and on and on.
Here is the control.
<xfx:XfxEntry
Text="{Binding OdometerEntry, StringFormat='{0:N0}'}"
TextColor="{Binding TextColor}"
IsEnabled="{Binding READ_ONLY, Converter={StaticResource NegateBool}}"
FontSize="Medium"
Keyboard="Numeric"
Margin="10,-20,0,-20"
Grid.Row="2"
Grid.Column="1" />
And here is the model for that field:
public string OdometerEntry
{
get => _odometerEntry;
set
{
if (string.IsNullOrEmpty(value) || value == "0")
{
if (Inspection != null && Inspection.Truck != null)
{
Inspection.Truck.OdometerEntry = 0;
}
_odometerEntry = "";
}
else
{
int i = General.IntParseSafe(value);
if (i < 0)
{
i = 0;
}
if (Inspection != null && Inspection.Truck != null)
{
Inspection.Truck.OdometerEntry = i;
}
_odometerEntry = i.ToString();
}
OnPropertyChanged();
}
}
I tried adding formatting in the ToString but that did not work. I realize this form extender is no longer supported, but I'm hoping someone has some insight.
UPDATE
I tried using the regular Entry tag but same result. The string just will not format.
<Entry Text="{Binding OdometerEntry,StringFormat='{0:#,0}'}"

Rough sketch of the solution, off the top of my head:
int _numericValue;
string _previousString = "";
override OnTextChanged(...)
{
string newText = ...; // From a parameter to OnTextChanged.
if (string.Equals(newText, _previousString))
return; // Avoid infinite loop - this call caused by our change!
string formattedString = StringFormat("...", newText); // Apply desired format.
if (!string.Equals(formattedString, _previousString))
{
_previousString = formattedString;
// CAUTION: This triggers another call to `OnTextChanged`.
theTextField.Text = formattedString;
// TODO: Set cursor to end of text?
}
}

Related

Is it better to put my logic in an event handler or in a setter for MVVM (Xamarin `Picker` `SelectedItem` quirks)

SOLUTION IS IN EDIT OF THE ACCEPTED ANSWER
I have a view in which has two Pickers, I need to have it so that when the SelectedItem property in one Picker changes, the list of Items in the second Picker (ItemSource) changes as well.
Currently I have a bound the SelectedItem and SelectedIndex properties of both pickers to properties in my ViewModel. In the setter(s) for both of them, I perform the logic needed to change the list of Items in the second picker. The list of Items in the second picker changes successfully, but when I set the SelectedIndex (to make it select an Item by default), this fails if the index which I am setting it to is the same as the index which it was on in the previous list. It just shows the Title of the Picker instead, this issue seems to be related to this bug.
My Code:
Both Pickers are bound to an Observable collection of strings FYI
FrameType and DirectionType are Enums
I initially used only the SelectedItem property
Relevant XAML
<Label Grid.Row="1" Grid.Column="0" VerticalTextAlignment="Center"
Text="Direction: " />
<Picker x:Name="PickerDirection" Grid.Row="1" Grid.Column="1"
Title="Select Direction"
ItemsSource="{Binding Directions}"
SelectedItem="{Binding SelectedDirection}"
SelectedIndex="{Binding SelectedDirectionIndex}"></Picker>
<Label Grid.Row="2" Grid.Column="0" VerticalTextAlignment="Center"
Text="Frame: "/>
<Picker x:Name="PickerFrame" Grid.Row="2" Grid.Column="1"
Title="Select Frame"
ItemsSource="{Binding Frames}"
SelectedItem="{Binding SelectedFrame}"
SelectedIndex="{Binding SelectedFrameIndex}"></Picker>
Relevant View Model code:
public string SelectedDirection
{
get { return _selectedDirection; }
set
{
if (Directions.Contains(value))
{
if (_selectedDirection != value)
{
if (EnumUtils.ToEnumFromString<FrameType>(SelectedFrame) == FrameType.Road &&
!DirectionsRoad.Contains(value))
{
_selectedDirection = Directions[Directions.IndexOf(DirectionType.Right.ToString())];
}
else
{
_selectedDirection = value;
}
OnPropertyChanged();
}
}
}
}
public string SelectedFrame
{
get { return _selectedFrame; }
set
{
if (_selectedFrame != value)
{
_selectedFrame = value;
if (EnumUtils.ToEnumFromString<FrameType>(_selectedFrame) == FrameType.Road)
{
Directions = DirectionsRoad;
if (Directions.Contains(SelectedDirection))
{
SelectedDirectionIndex = Directions.IndexOf(SelectedDirection);
}
else
{
SelectedDirectionIndex = Directions.IndexOf(DirectionType.Right.ToString());
}
}else if (EnumUtils.ToEnumFromString<FrameType>(_selectedFrame) == FrameType.Lane)
{
Directions = DirectionsAll;
SelectedDirectionIndex = Directions.IndexOf(SelectedDirection);
}
OnPropertyChanged();
}
}
}
}
public int SelectedDirectionIndex
{
get { return _selectedDirectionIndex; }
set
{
if (_selectedDirectionIndex != value)
{
if (!(value < 0 || value >= Directions.Count))
{
_selectedDirectionIndex = value;
OnPropertyChanged();
}
}
}
}
public int SelectedFrameIndex
{
get { return _selectedFrameIndex; }
set
{
if (_selectedFrameIndex != value)
{
if (!(value < 0 || value >= Frames.Count))
{
_selectedFrameIndex = value;
OnPropertyChanged();
}
}
}
}
The outcome:
I expect it to never be empty since I ensure that the SelectedDirection is always set to something.
Notes:
I initially used just the SelectedItem property, but when I encountered this bug I thought using the SelectedIndex property would help to fix it.
I used ObservableCollection to maintain consistency with the other viewModels in the project, and to ensure that the options in the picker are updated when I make changes (based on my understanding you need to use ObservableCollection to make this possible).
I do plan to refactor the code in the setter for SelectedFrame into smaller functions as soon as I get things to work.
Due to this It seems that using the SelectedIndexChanged event of the Picker would be the only way to fix this. However the comment by ottermatic in this question says that events are unreliable. Hence I felt is was better to perform this logic in the setter.
If someone could comment on what I may be doing wrong in my code which is causing this issue and also comment on the pros/cons and/or whether or not I should use the eventHandler or have the logic in my setter. Thanks
I don't really see why you are using both SelectedItem and SelectedIndex, but I think what you are trying to achieve can be achieved easier.
First of all, I don't think you need ObservableCollection types at all in your example, since you are setting the directions anyway and not modifying the collection. More importantly, fiddling around with the indices is completely unnecessary, as far as I can tell. You are using strings anyway and even though String is not a value type, but a reference type, you cannot practically distinguish two String instances that have the same content, hence assinging the respective values to SelectedDirection and SelectedFrame is sufficient.
The following checks seem redundant to me
if (Directions.Contains(value))
if (EnumUtils.ToEnumFromString<FrameType>(SelectedFrame) == FrameType.Road &&
!DirectionsRoad.Contains(value))
since Directions are set to DirectionsRoad anyway if SelectedFrame has been set to "Road". Hence I'd assume that the second condition won't evaluate to true in any case. Hence the SelectedDirection can be simplified:
public string SelectedDirection
{
get => _selectedDirection;
set
{
if (_selectedDirection != value && Directions.Contains(value))
{
_selectedDirection = value;
OnPropertyChanged();
}
}
}
Within the setter of SelectedFrame there are many things going on, which I'd refactor to methods on it's own right, to improve clarity.
public string SelectedFrame
{
get => _selectedFrame;
set
{
if (_selectedFrame != value)
{
_selectedFrame = value;
UpdateAvailableDirections();
OnPropertyChanged();
}
}
}
private void UpdateAvailableDirections()
{
// store the selected direction
var previouslySelectedDirection = SelectedDirection;
Directions = GetValidDirectionsByFrameType(EnumUtils.ToEnumFromString<FrameType>(SelectedFrame));
SelectedDirection = GetSelectedDirection(previoslySelectedDirection, Directions);
}
private string[] GetValidDirectionsByFrameType(FrameType frameType)
{
return frameType == FrameType.Road ? DirectionsRoad : DirectionsAll;
}
private string GetSelectedDirection(string previouslySelectedDirection, string[] validDirections)
{
return validDirections.Contains(previouslySelectedDirection) ? previouslySelectedDirection : DefaultDirection;
}
By setting the SelectedItem instead of fiddling with the indices, the correct values shall be displayed.
Concerning your question whether this logic may be better suited in an event handler or in the setter depends on your requirements. If all you need is the index, the event SelectedIndexChanged may work out for you, but if the value is needed in several places and methods that are not called by the event handler, the presented solution may be more viable.
Edit
You were correct, it has got nothing to do with the usage of SelectedIndex and SelectedItem. The issue is a bit more subtle.
I build a quick proof-of-concept and found the following:
Assuming SelectedDirection is "Right" (and the index is set accordingly)
When Directions is set, the SelectedItem on the picker seems to be reset
SelectedDirection is set to null
this.Directions.Contains(value) evaluates to false, hence _selectedDirection is not set (this hold true for SelectedDirectionIndex, since the value -1 is filtered by if(!value < 0 || value >= this.Directions.Count))
When SelectedDirection is set afterwards, the value is still "Right", hence OnPropertyChanged is not called (since the values are the same) and the SelectedItem is not set
This way there is a mismatch between the value the Picker actually holds and the property in the viewmodel, which leads to unexpected behavior.
How to mitigate the issue?
I'd still stick with the code without the indices (unless you really need them) and use the string values.
There are other possibilities, but I'd change the setter of SelectedDirection. When you allowed the value to be set to null, PropertyChanged will be raised properly when the value is set to Right afterwards. If you really need to filter what the value is set to, you should still raise OnPropertyChanged, to inform the Picker that the value has changed (preventing a mismatch between the actual Pickers value and the viewmodel)
public string SelectedDirection
{
get => _selectedDirection;
set
{
if (_selectedDirection != value)
{
if(Directions.Contains(value))
{
_selectedDirection = value;
}
else
{
_selectedDirection = DirectionTypes.Right.ToString();
}
OnPropertyChanged();
}
}
}
Found a somewhat hacky fix for this, and it seems to be a Xamarin issue. I have made the following changes to my code"
The relevant changes are in the setter for SelectedFrame:
public string SelectedFrame
{
get { return _selectedFrame; }
set
{
if (_selectedFrame != value)
{
_selectedFrame = value;
if (EnumUtils.ToEnumFromString<FrameType>(_selectedFrame) == FrameType.Road)
{
Directions = DirectionsRoad;
if (Directions.Contains(SelectedDirection))
{
/*Relevant edits*/
var position = Directions.IndexOf(SelectedDirection);
SelectedDirection = Directions[Directions.Count - position -1];
SelectedDirection = Directions[position];
}
else
{
SelectedDirectionIndex = Directions.IndexOf(DirectionType.Right.ToString());
}
}else if (EnumUtils.ToEnumFromString<FrameType>(_selectedFrame) == FrameType.Lane)
{
Directions = DirectionsAll;
/*Relevant edits*/
var position = Directions.IndexOf(SelectedDirection);
SelectedDirection = Directions[Directions.Count - position -1];
SelectedDirection = Directions[position];
}
OnPropertyChanged();
}
}
}
}
It seems that my issue arises when I change the contents of my ObservableCollectoin but the SelectedDirection stays the same.
When I change Directions (which is an ObservableCollection) by assigning it to DirectionsAll (also an ObservableCollection), I need to make sure that the SelectedDirection changes,. The added code ensures that a change actually occurs to SelectionDirection and that fixes it. Seems somewhat hacky but it works.
Outcome:

How can I calculate what the Text of my WPF TextBox will be following these events?

I want to restrict this TextBox to only take positive integers up to and including Int32.MaxValue:
<TextBox Name="CurrentPageNumber"
PreviewTextInput="CurrentPageNumber_PreviewTextInput"
DataObject.Pasting="CurrentPageNumber_Pasting" />
I have the following events:
private void CurrentPageNumber_Pasting(object sender, DataObjectPastingEventArgs e)
{
if (!e.DataObject.GetDataPresent(typeof(String)))
e.CancelCommand();
String text = (String)e.DataObject.GetData(typeof(String));
if (!IsPositiveInteger(text))
e.CancelCommand();
}
private bool IsPositiveInteger(String text)
{
if (text.Length <= 0 || ((int)text[0] == 48 && text.Length != 1)) // Restricts empty strings and numbers with preceding 0's except for 0 itself.
return false;
for (int i = 0; i < text.Length; i++)
{
var c = (int)text[i];
if (c < 48 || c > 57) // Check that all characters are between 0 and 9.
return false;
}
int result;
return Int32.TryParse(text, out result);
}
This is not good enough, because someone can have the equivalent integer value of Int32.MaxValue (2147483647) in the text box already, and then add another 1 to the right of it. How could it be possible to use my IsPositiveInteger method by predicting the outcome of the Text following the event's operation?
This is not the same as preventing the change to the text, but it's the customary way of telling a user "you can't put that text here". I recommend doing it this way because this is the way the framework supports, so it's easiest, and usually what the framework supports is what people are most accustomed to seeing in user interfaces. What we're accustomed to, we call "intuitive".
When the user changes the text in that textbox, the ValidationRule gets invoked. If it returns a false ValidationResult, the user gets a red error border, the bound property is not updated, and the tooltip tells him what he did wrong.
If you really want to stick with your original idea, you're in for a lot of work, as you found out. I don't think the ROI justifies the effort, but when I was younger I once wrote a binary adder in Perl, so I'm in no position to throw any stones.
The conventional way to do this type of thing in WPF is using a ValidationRule on the Binding. You don't have a Binding because you don't have a viewmodel. This is a nearly incomprehensible heresy and it chills me to the very marrow, but we'll work with it. We'll add a property to your codebehind just to have something to bind to.
public int NonNegativeIntValue { get; set; }
If you want to be able to update the textbox by setting that property in your codebehind, you'll have to go the INotifyPropertyChanged route. Better yet, use a viewmodel; every time you do anything without a viewmodel in WPF, it turns out you'd have been better off with one. The framework is like a traffic cop; acknowledge its authority and it'll be easier to get along with.
And then we'll write a ValidationRule:
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Windows.Controls;
namespace ValidationRules
{
public class PositiveIntegerRule : ValidationRule
{
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
int result;
if (value.GetType() == typeof(String) && Int32.TryParse((String)value, out result) && result > 0)
return new ValidationResult(true, null);
return new ValidationResult(false, "Value must be a positive integer");
}
}
}
And finally we'll fix your TextBox:
<TextBox
Name="CurrentPageNumber"
xmlns:vr="clr-namespace:ValidationRules"
ToolTip="{Binding RelativeSource={RelativeSource Self}, Path=(Validation.Errors)[0].ErrorContent}"
>
<TextBox.Text>
<Binding
Path="NonNegativeIntValue"
RelativeSource="{RelativeSource AncestorType=Window}"
>
<Binding.ValidationRules>
<vr:PositiveIntegerRule />
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
I'm making one assumption here: That your codebehind class inherits from Window. If it's UserControl, you'll have to change RelativeSource AncestorType=Window in the binding to RelativeSource AncestorType=UserControl -- whatever's right.
Another way to do it other than Ed's answer is like this, this way will ensure the UI always has a usable, visible value displayed, but it is not as clean and so I recommend people go with validation rules as the other answer has (its more WPF-like). Also, I had an error with my IsPositiveInteger method; originally I wanted 0 but now I realize since I am paging data I want 1 as the beginning page.
XAML:
<TextBox Name="CurrentPageNumber" TextChanged="CurrentPageNumber_TextChanged"/>
C#:
private bool IsPositive32BitInteger(String text)
{
if (text.Length <= 0 || (int)text[0] == 48) // Restricts empty strings and numbers with preceding 0's or 0 itself.
return false;
for (int i = 0; i < text.Length; i++)
{
var c = (int)text[i];
if (c < 48 || c > 57) // Check that all characters are between 0 and 9.
return false;
}
int result;
return Int32.TryParse(text, out result);
}
private void CurrentPageNumber_TextChanged(object sender, TextChangedEventArgs e)
{
var textBox = (TextBox)sender;
if (textBox.Text.Length <= 0)
{
textBox.Text = "1";
return; // Set text to "1" as default and return. The event will fire again since we set the Text property.
}
if (textBox.Text.StartsWith("0"))
{
textBox.Text = textBox.Text.TrimStart(new char[] { '0' });
return; // Trim the text and return. The event will fire again since we set the Text property.
}
if (!IsPositive32BitInteger(textBox.Text)) {
textBox.Text = "1";
return; // Set text to "1" as default and return. The event will fire again since we set the Text property.
}
// At this point the value is forced into the state you want and you can do other stuff.
}

IDataErrorInfo not updated

I encountered a problem with the IDataErrorInfo Interface and a wizard I'm currently programming.
The intention of my programm is to ask some Inputs ( usually done with a barcode scanner) and depending on the inputs start a specific sequence.
This is working as intendet. To make sure to catch wrong scans all inputs are check with an event ( OnValueParseFailed) If this event is triggered my current textbox is focused and all text selected:
this.MyWizardViewModel.ValueParseFailed += (s, e) =>
{
switch (e.Parameter)
{
case "ProductionOrder":
this.TextBoxProduction.Focus();
this.TextBoxProduction.SelectAll();
break;
The Interface itself is included this way:
public string this[string name]
{
get
{
string result = null;
if ((name == "ProductionOrder") && (!string.IsNullOrEmpty(this.ProductionOrder)))
{
if (this.System.FirmwareVersion == 0)
result = Lang.Strings.WrongEntry;
}
Its working for the first run. But if the wizard is finished or aborted and run a second time without closing the app, no error message is shown.
The Reset simply returns the app to default values.
public void ResetApplikation()
{
this.System.Clear(); // reset System values
this.ProductionOrder = string.Empty;
this.BmsTypeCode = string.Empty;
this.CellStack1TypeCode = string.Empty;
this.CellClass1 = string.Empty;
this.CellStack2TypeCode = string.Empty;
this.CellClass2 = string.Empty;
this.IsSystemProgrammed = false;
this.IsSystemParameterized = false;
this.MyMachine.Abort(); // reset wizard state
}
While debugging I can see the Interface to be handeled correctly. But no error is displayed.
In XAML the binding is set TwoWay
<TextBox Name="TextBoxProduction" Grid.Row="2" Width="200" Margin="10"
Style="{StaticResource TextBoxNormal}" Loaded="TextBoxProduction_Loaded"
Text="{Binding Path=ProductionOrder, ValidatesOnDataErrors=True,
NotifyOnValidationError=True, Delay=100,
UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" />
I'm using MahApps but as the textbox class is based on the wpf textbox I doubt a bug in this element is the problem. Any suggestions would be great.
Thank you.
The Answer of Domysee helped me.
Implementing INotifyDataErrorInfo instead of IDataErrorInfo was a major change but it fixed the problem!

Dynamically formatted TextBlock

I would like to format specific words in TextBlock dynamically, because it is binded to my object. I was thinking about using Converter but using following solution add only tags directly to the text (instead of showing it formatted).
public class TextBlockFormatter : IValueConverter {
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
string regexp = #"\p{L}+#\d{4}";
if (value != null) {
return Regex.Replace(value as string, regexp, m => string.Format("<Bold>{0}</Bold>", m.Value));
}
return null;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
return null;
}
}
This is not an attempt to answer this question... it is a demonstration in response to a question in the question comments.
Yes #Blam, you can format individual words, or even characters in a TextBlock... you need to use the Run (or you could replace Run with TextBlock) class. Either way, you can also data bind to the Text property on either of them:
<TextBlock HorizontalAlignment="Center" VerticalAlignment="Center">
<Run Text="This" FontWeight="Bold" Foreground="Red" />
<Run Text="text" FontSize="18" />
<Run Text="is" FontStyle="Italic" />
<Run Text="in" FontWeight="SemiBold" Background="LightGreen" />
<Run Text="one" FontFamily="Candara" FontSize="20" />
<Run Text="TextBlock" FontWeight="Bold" Foreground="Blue" />
</TextBlock>
UPDATE >>>
Regarding this question now, I suppose that it would be possible to use these Run elements in a DataTemplate and have them generated from the data... the data in this case would have to be classes with (obviously) a Text property, but also formatting properties that you could use to data bind to the Run style properties.
It would be awkward though because the TextBlock has no ItemsSource property that you could bind your collection of word classes to... maybe you could use a Converter for that part... just thinking aloud here... I'm going to stop now.
UPDATE >>>
#KrzysztofBzowski, unfortunately the TextBlock.Inlines property is not a DependencyProperty, so you won't be able to data bind to it. However, it got me thinking and I did another search and found the Binding text containing tags to TextBlock inlines using attached property in Silverlight for Windows Phone 7 article on Jevgeni Tsaikin's .NET laboratory.
It would involve you declaring an Attached Property and a Converter, but it looks promising... give it a go. And don't worry that it's for Silverlight... if it works in Silverlight, then it'll work in WPF.
I recently had to solve this problem which I was able to do by writing a Blend behaviour for TextBlocks.
It can be declared in XAML with a list of Highlight elements where you specify the text to highlight, the colour you want that text to be and it's font weight (can easily add more formatting properties as required).
It works by looping though the desired highlights, scanning the TextBlock for each phrase starting at the TextBlock.ContentStart TextPointer. Once the phrase is found it can build a TextRange which can have the formatting options applied to it.
It should work if the TextBlock Text property is data bound too because I attach to the bindings Target updated event.
See below for the behaviour code and an example in XAML
public class TextBlockHighlightBehaviour : Behavior<TextBlock>
{
private EventHandler<DataTransferEventArgs> targetUpdatedHandler;
public List<Highlight> Highlights { get; set; }
public TextBlockHighlightBehaviour()
{
this.Highlights = new List<Highlight>();
}
#region Behaviour Overrides
protected override void OnAttached()
{
base.OnAttached();
targetUpdatedHandler = new EventHandler<DataTransferEventArgs>(TextBlockBindingUpdated);
Binding.AddTargetUpdatedHandler(this.AssociatedObject, targetUpdatedHandler);
// Run the initial behaviour logic
HighlightTextBlock(this.AssociatedObject);
}
protected override void OnDetaching()
{
base.OnDetaching();
Binding.RemoveTargetUpdatedHandler(this.AssociatedObject, targetUpdatedHandler);
}
#endregion
#region Private Methods
private void TextBlockBindingUpdated(object sender, DataTransferEventArgs e)
{
var textBlock = e.TargetObject as TextBlock;
if (textBlock == null)
return;
if(e.Property.Name == "Text")
HighlightTextBlock(textBlock);
}
private void HighlightTextBlock(TextBlock textBlock)
{
foreach (var highlight in this.Highlights)
{
foreach (var range in FindAllPhrases(textBlock, highlight.Text))
{
if (highlight.Foreground != null)
range.ApplyPropertyValue(TextElement.ForegroundProperty, highlight.Foreground);
if(highlight.FontWeight != null)
range.ApplyPropertyValue(TextElement.FontWeightProperty, highlight.FontWeight);
}
}
}
private List<TextRange> FindAllPhrases(TextBlock textBlock, string phrase)
{
var result = new List<TextRange>();
var position = textBlock.ContentStart;
while (position != null)
{
var range = FindPhrase(position, phrase);
if (range != null)
{
result.Add(range);
position = range.End;
}
else
position = null;
}
return result;
}
// This method will search for a specified phrase (string) starting at a specified position.
private TextRange FindPhrase(TextPointer position, string phrase)
{
while (position != null)
{
if (position.GetPointerContext(LogicalDirection.Forward) == TextPointerContext.Text)
{
string textRun = position.GetTextInRun(LogicalDirection.Forward);
// Find the starting index of any substring that matches "phrase".
int indexInRun = textRun.IndexOf(phrase);
if (indexInRun >= 0)
{
TextPointer start = position.GetPositionAtOffset(indexInRun);
TextPointer end = start.GetPositionAtOffset(phrase.Length);
return new TextRange(start, end);
}
}
position = position.GetNextContextPosition(LogicalDirection.Forward);
}
// position will be null if "phrase" is not found.
return null;
}
#endregion
}
public class Highlight
{
public string Text { get; set; }
public Brush Foreground { get; set; }
public FontWeight FontWeight { get; set; }
}
Example usage in XAML:
<TextBlock Text="Here is some text">
<i:Interaction.Behaviors>
<behaviours:TextBlockHighlightBehaviour>
<behaviours:TextBlockHighlightBehaviour.Highlights>
<behaviours:Highlight Text="some" Foreground="{StaticResource GreenBrush}" FontWeight="Bold" />
</behaviours:TextBlockHighlightBehaviour.Highlights>
</behaviours:TextBlockHighlightBehaviour>
</i:Interaction.Behaviors>
</TextBlock>
You'll need to import the Blend interactivity namespace and your behaviour's namespace:
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:behaviours="clr-namespace:YourProject.Behviours"
I had a similar use case where I needed to build a text editor with RichTextBox. However, all text formatted changes: Font, Color, Italic, Bold must reflect on the TextBlock dynamically. I found few articles pointing me to Textblock.inlines.Add() which seemed helpful but only allow one change at a time or appending to the existing text.
However, Textblock.inlines.ElementAt(index of the existing text to format) can be utilized to apply the desired text format to the text located at that index. Below is my pseudo approach to resolving this issue. I hope this helps:
For RichTextBox:
selectedText.ApplyPropertyValue(TextElement.FontFamilyProperty, cbFontFamily.SelectedValue.ToString());
However, for the Textblock formatting to work I had to use Run run = new Run() concept which allows and works with:
Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString(SelectedColorText))
FontFamily = new FontFamily(cbFontFamily.SelectedValue.ToString())
FontStyle = FontStyles.Italic
TextDecorations = TextDecorations.Underline
TextDecorations = TextDecorations.Strikethrough
FontWeight = (FontWeight)new FontWeightConverter().ConvertFromString(cbFontWeightBox.SelectedItem.ToString())
Additionally, I created a class with various fields and constructors. I also created a custom based class dictionary to capture all the changes made in the RichTextbbox. Finally, I applied all the captured information in the dictionary via a forloop.
TextBlock.Inlines.ElementAt(mItemIndex).Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString(dictionaryItem.Value._applyColor.ToString()));
TextBlock.Inlines.ElementAt(mItemIndex).FontFamily = new FontFamily(ftItem.Value._applyFontFamily.ToString());
TextBlock.Inlines.ElementAt(mItemIndex).FontStyle = FontStyles.Italic;
TextBlock.Inlines.ElementAt(mItemIndex).TextDecorations = TextDecorations.Underline;
TextBlock.Inlines.ElementAt(mItemIndex).TextDecorations = TextDecorations.Strikethrough;

RaisePropertyChanged fails to update the UI

I've been working on an application in MVVM Light lately. I have a TextBox in my XAML bound to my UI. I'd like to validate any input and ensure that only numbers are entered. I've tried the following code:
My TextBox:
<TextBox TabIndex="1" Height="23" MinWidth="410" DockPanel.Dock="Left"
HorizontalAlignment="Left"
Text="{Binding Input, UpdateSourceTrigger=PropertyChanged}"
IsEnabled="{Binding IsEnabled}"
AcceptsReturn="False"
local:FocusExtension.IsFocused="{Binding IsFocused}">
And in my ViewModel:
private string input;
public string Input
{
get { return this.input; }
set
{
decimal test;
if(decimal.TryParse(value, out test))
{
this.input = value;
}
else
{
this.input = "";
}
RaisePropertyChanged("Input");
}
}
This fails to update the UI. If I enter "B" and check the debugger, it runs through the setter, but fails to actually update the UI.
Curiously, if I set this.input = "TEST"; in the else block, the UI updates, but, if I attempt to set it to "", string.Empty, or the value of input before the validation, the UI fails to update.
Is this by design? Possibly a bug? Is there something I'm doing wrong?
Edit I mistakenly forgot to include RaisePropertyChanged in my example code. I've updated it. Raising it isn't the problem as I've watched the debugger run all the way through raising it and returning input via the getter.
Way you use strign type property and then convert to decimal, easier to change lik this:
public decimal Input
{
get { return this.input; }
set
{
this.input = value;
RaisePropertyChanged("Input");
}
}
And for validate use IDataErrorInfo (read more: http://blogs.msdn.com/b/wpfsdk/archive/2007/10/02/data-validation-in-3-5.aspx)
What we have done is created a Custom Control, since we use it for a Currency Text Box. I warn you I have no validation that this is a good idea, or falls in line with MVVM model because all manipulation of the control are done in code behind.
In the control on the textbox we have an event on PreviewTextInput that does this
e.Handled = Functions.DoubleConverter(Convert.ToChar(e.Text), ((TextBox)sender).Text.Replace("$", ""));
Then for the function (which isnt perfect, I have a few issues with it still) is:
static public bool DoubleConverter(char c, string str)
{
if (!char.IsDigit(c))
{
if (c == '.' && (str.Contains('.')))
{
return true;
}
else if (c != '.')
{
return true;
}
}
return false;
}
Please use this as a reference, not exactly as is because it is a very crude implementation.

Categories

Resources