My WPF App receives a stream of messages from a backend service that I need to display in the UI. These messages vary widely and I want to have different visual layout (string formats, colors, Fonts, icons, whatever etc.) for each message.
I was hoping to just be able to create an inline (Run, TextBlock, Italic etc) for each message then somehow put them all in a ObservableCollection<> and using he magic of WPF Data Binding on my TextBlock.Inlines in the UI. I couldn't find how to do this, is this possible?
You could add a Dependency Property to a TextBlock Subclass
public class BindableTextBlock : TextBlock
{
public ObservableCollection<Inline> InlineList
{
get { return (ObservableCollection<Inline>)GetValue(InlineListProperty); }
set { SetValue(InlineListProperty, value); }
}
public static readonly DependencyProperty InlineListProperty =
DependencyProperty.Register("InlineList",typeof(ObservableCollection<Inline>), typeof(BindableTextBlock), new UIPropertyMetadata(null, OnPropertyChanged));
private static void OnPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
BindableTextBlock textBlock = sender as BindableTextBlock;
ObservableCollection<Inline> list = e.NewValue as ObservableCollection<Inline>;
list.CollectionChanged += new System.Collections.Specialized.NotifyCollectionChangedEventHandler(textBlock.InlineCollectionChanged);
}
private void InlineCollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Add)
{
int idx = e.NewItems.Count -1;
Inline inline = e.NewItems[idx] as Inline;
this.Inlines.Add(inline);
}
}
}
This is not possible because the TextBlock.Inlines property is not a dependency property. Only dependency properties can be the target of a data binding.
Depending on your exact layout requirements you may be able to do this using an ItemsControl, with its ItemsPanel set to a WrapPanel and its ItemsSource set to your collection. (Some experimentation may be required here because an Inline is not a UIElement, so its default rendering will probably be done using ToString() rather than being displayed.)
Alternatively, you may need to build a new control, e.g. MultipartTextBlock, with a bindable PartsSource property and a TextBlock as its default template. When the PartsSource was set your control would attach a CollectionChanged event handler (directly or via CollectionChangedEventManager), and update the TextBlock.Inlines collection from code as the PartsSource collection changed.
In either case, caution may be required if your code is generating Inline elements directly (because an Inline can't be used in two places at the same time). You may alternatively want to consider exposing an abstract model of text, font, etc. (i.e. a view model) and creating the actual Inline objects via a DataTemplate. This may also improve testability, but obviously adds complexity and effort.
This is an alternative solution which utilizes WPF behaviors/attached properties:
public static class TextBlockExtensions
{
public static IEnumerable<Inline> GetBindableInlines ( DependencyObject obj )
{
return (IEnumerable<Inline>) obj.GetValue ( BindableInlinesProperty );
}
public static void SetBindableInlines ( DependencyObject obj, IEnumerable<Inline> value )
{
obj.SetValue ( BindableInlinesProperty, value );
}
public static readonly DependencyProperty BindableInlinesProperty =
DependencyProperty.RegisterAttached ( "BindableInlines", typeof ( IEnumerable<Inline> ), typeof ( TextBlockExtensions ), new PropertyMetadata ( null, OnBindableInlinesChanged ) );
private static void OnBindableInlinesChanged ( DependencyObject d, DependencyPropertyChangedEventArgs e )
{
var Target = d as TextBlock;
if ( Target != null )
{
Target.Inlines.Clear ();
Target.Inlines.AddRange ( (System.Collections.IEnumerable) e.NewValue );
}
}
}
In your XAML, use it like this:
<TextBlock MyBehaviors:TextBlockExtensions.BindableInlines="{Binding Foo}" />
This saves you from having to inherit from TextBlock. It could just as well work using an ObservableCollection instead of IEnumerable, in that case you'd need to subscribe to collection changes.
In version 4 of WPF you will be be able to bind to a Run object, which may solve your problem.
I have solved this problem in the past by overriding an ItemsControl and displaying the text as items in the ItemsControl. Look at some of the tutorials that Dr. WPF has done on this kind of stuff: http://www.drwpf.com
Thanks Frank for your solution. I had to make a couple of minor changes to make it work for me.
public class BindableTextBlock : TextBlock
{
public ObservableCollection<Inline> InlineList
{
get { return (ObservableCollection<Inline>) GetValue(InlineListProperty); }
set { SetValue(InlineListProperty, value); }
}
public static readonly DependencyProperty InlineListProperty =
DependencyProperty.Register("InlineList", typeof (ObservableCollection<Inline>), typeof (BindableTextBlock), new UIPropertyMetadata(null, OnPropertyChanged));
private static void OnPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
BindableTextBlock textBlock = (BindableTextBlock) sender;
textBlock.Inlines.Clear();
textBlock.Inlines.AddRange((ObservableCollection<Inline>) e.NewValue);
}
}
If i am getting your requirement correctly, you can manually check for the coming messages and for each message you can add an element to TextBlock.Inlines property. It will not take any DataBinding.
I have done this with the following:
public string MyBindingPath
{
get { return (string)GetValue(MyBindingPathProperty); }
set { SetValue(MyBindingPathProperty, value); }
}
// Using a DependencyProperty as the backing store for MyBindingPath. This enables animation, styling, binding, etc...
public static readonly DependencyProperty MyBindingPathProperty =
DependencyProperty.Register("MyBindingPath", typeof(string), typeof(Window2), new UIPropertyMetadata(null, OnPropertyChanged));
private static void OnPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
(sender as Window2).textBlock.Inlines.Add(new Run(e.NewValue.ToString()));
}
The Suggestion from Pavel Anhikouski works perfectly. Here the missing part with databinding in MVVM. Use the AddTrace property in the viewmodel to add content to the OutputBlock in the window.
The backing property MyBindingPath in the window is not needed.
ViewModel:
private string _addTrace;
public string AddTrace
{
get => _addTrace;
set
{
_addTrace = value;
NotifyPropertyChanged();
}
}
public void StartTrace()
{
AddTrace = "1\n";
AddTrace = "2\n";
AddTrace = "3\n";
}
TraceWindow.xaml:
<Grid>
<ScrollViewer Name="Scroller" Margin="0" Background="#FF000128">
<TextBlock Name="OutputBlock" Foreground="White" FontFamily="Consolas" Padding="10"/>
</ScrollViewer>
</Grid>
TraceWindow.xaml.cs:
public TraceWindow(TraceWindowModel context)
{
DataContext = context;
InitializeComponent();
//bind MyBindingPathProperty to AddTrace
Binding binding = new Binding("AddTrace");
binding.Source = context;
this.SetBinding(MyBindingPathProperty, binding);
}
public static readonly DependencyProperty MyBindingPathProperty =
DependencyProperty.Register("MyBindingPath", typeof(string), typeof(TraceWindow), new UIPropertyMetadata(null, OnPropertyChanged));
private static void OnPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
(sender as TraceWindow).OutputBlock.Inlines.Add(new Run(e.NewValue.ToString()));
}
Most recently I had a similar task to solve, namely; having unlimited number of url links inserted to a custom message box text content, and have a binding path to this text.
I decided to post my implementation here seeing that this thread had some evolution of different great ideas... Here is my solution:
The concept:
The flow of xaml TextBlock content:
<TextBlock>
...
<Inline>
<Hyperlink <Inline>>
<Inline>
<Hyperlink <Inline>>
...
My x:Name=MixedText TextBlock element receives its value as a single text formated as:
"...some text here...[link-text|url-link]...some other text here... etc."
Sample:
"Please visit the Microsoft [site|https://www.microsoft.com/en-us/windows/windows-7-end-of-life-support-information], and download the Windows 7 SP1, complete the SP1 installation then re-run the installer again. Go to [roblox|https://www.roblox.com] site to relax a bit like my son \u263A."
I do my parsing and all elements' injection to my MixedText TextBlock element at the DataContextChanged event.
The xaml part: Defining the binding path (MixedText).
...
<TextBlock Grid.Row="3" Grid.Column="1"
x:Name="HyperlinkContent"
TextWrapping="Wrap"
VerticalAlignment="Top"
HorizontalAlignment="Left"
Text="{Binding Path = MixedText}">
</TextBlock>
The ViewModel part: Defining the binding path property.
public string MixedText
{
get { return _mixedText; }
set
{
_mixedText = value;
OnPropertyChanged();
}
}
string _mixedText;
The MultipartTextHandler class where I implement the MixedText parsing and dynamic xaml injection model preparation.
class MultipartTextHandler
{
public static IEnumerable<(int Index, Type Type, object Control, string Text, bool IsHyperlink)> CreateControls(string multipartText)
{
// 1. Return null if no multipart text is found. This will be just an ordinary text passed to a binding path.
var multipartTextCollection = GetMultipartTextCollection(multipartText);
if (!multipartTextCollection.Any())
return Enumerable.Empty<(int Index, Type Type, object Control, string Text, bool IsHyperlink)>();
var result = new List<(int Index, Type Type, object Control, string Text, bool IsHyperlink)>();
// 2. Process multipart texts that have Hyperlink content.
foreach (var e in multipartTextCollection.Where(x => x.Hyperlink != null))
{
var hyperlink = new Hyperlink { NavigateUri = new Uri(e.Hyperlink) };
hyperlink.Click += (sender, e1) => Process.Start(new ProcessStartInfo(new Uri(e.Hyperlink).ToString()));
hyperlink.Inlines.Add(new Run { Text = e.Text });
result.Add((Index: e.Index, Type: typeof(Hyperlink), Control: hyperlink, Text: e.Text, IsHyperlink: true));
}
// 3. Process multipart texts that do not have Hyperlink content.
foreach (var e in multipartTextCollection.Where(x => x.Hyperlink == null))
{
var inline = new Run { Text = e.Text };
result.Add((Index: e.Index, Type: typeof(Inline), Control: inline, Text: e.Text, IsHyperlink: false));
}
return result.OrderBy(x => x.Index);
}
/// <summary>
/// Returns list of Inline and Hyperlink segments.
/// Parameter sample:
/// "Please visit the Microsoft [site|https://www.microsoft.com/en-us/windows/windows-7-end-of-life-support-information], and download the Windows 7 SP1, complete the SP1 installation then re-run the installer again. Go to [roblox|https://www.roblox.com] site to relax a bit like my son ☀."
/// </summary>
/// <param name="multipartText">See sample on comment</param>
static IEnumerable<(int Index, string Text, string Hyperlink)> GetMultipartTextCollection(string multipartText)
{
// 1. Make sure we have a url string in parameter argument.
if (!ContainsURL(multipartText))
return Enumerable.Empty<(int Index, string Text, string Hyperlink)>();
// 2a. Make sure format of url link fits to our parsing schema.
if (multipartText.Count(x => x == '[' || x == ']') % 2 != 0)
return Enumerable.Empty<(int Index, string Text, string Hyperlink)>();
// 2b. Make sure format of url link fits to our parsing schema.
if (multipartText.Count(x => x == '|') != multipartText.Count(x => x == '[' || x == ']') / 2)
return Enumerable.Empty<(int Index, string Text, string Hyperlink)>();
var result = new List<(int Index, string Text, string Hyperlink)>();
// 3. Split to Inline and Hyperlink segments.
var multiParts = multipartText.Split(new char[] { '[', ']' }, StringSplitOptions.RemoveEmptyEntries);
foreach (var part in multiParts)
{
// Hyperlink segment must contain inline and Hyperlink splitter checked in step 2b.
if (part.Contains('|'))
{
// 4a. Split the hyperlink segment of the overall multipart text to Hyperlink's inline
// and Hyperlink "object" contents. Note that the 1st part is the text that will be
// visible inline text with 2nd part that will have the url link "under."
var hyperPair = part.Split(new char[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
// 4b. Add hyperlink record to the return list: Make sure we keep the order in which
// these values are set at multipartText. Note that Hyperlink's inline, and Hyperlink
// url texts are added to Text: and Hyperlink: properties separately.
result.Add((Index: result.Count + 1, Text: hyperPair[0], Hyperlink: hyperPair[1]));
}
else
{
// 5. This text will be an inline element either before or after the hyperlink element.
// So, Hyperlink parameter we will set null to later process differently.
result.Add((Index: result.Count + 1, Text: part, Hyperlink: null));
}
}
return result;
}
/// <summary>
/// Returns true if a text contains a url string (pattern).
/// </summary>
/// <param name="Text"></param>
/// <returns></returns>
static bool ContainsURL(string Text)
{
var pattern = #"([a-zA-Z\d]+:\/\/)?((\w+:\w+#)?([a-zA-Z\d.-]+\.[A-Za-z]{2,4})(:\d+)?(\/)?([\S]+))";
var regex = new Regex(pattern, RegexOptions.Compiled | RegexOptions.IgnoreCase);
return regex.IsMatch(Text);
}
}
The Code-behind stuff.
Inside the view constructor:
this.DataContextChanged += MessageBoxView_DataContextChanged;
The MessageBoxView_DataContextChanged implementation.
private void MessageBoxView_DataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
{
var viewModel = (MessageBoxViewModel)e.NewValue;
var mixedText = viewModel.MixedText;
var components = MultipartTextHandler.CreateControls(mixedText);
this.HyperlinkContent.Inlines.Clear();
this.HyperlinkContent.Text = null;
foreach (var content in components)
{
if (content.Type == typeof(Inline))
this.HyperlinkContent.Inlines.Add(new Run { Text = content.Text });
else if (content.Type == typeof(Hyperlink))
this.HyperlinkContent.Inlines.Add((Hyperlink)content.Control);
}
}
The usage, from my console application.
static void Test()
{
var viewModel = new MessageBox.MessageBoxViewModel()
{
MixedText = "Please visit the Microsoft [site|https://www.microsoft.com/en-us/windows/windows-7-end-of-life-support-information], and download the Windows 7 SP1, complete the SP1 installation then re-run the installer again. Go to [roblox|https://www.roblox.com] site to relax a bit like my son \u263A.",
};
var view = new MessageBox.MessageBoxView();
view.DataContext = viewModel; // Here is where all fun stuff happens
var application = new System.Windows.Application();
application.Run(view);
Console.WriteLine("Hello World!");
}
The actual dialog display view:
Imports System.Collections.ObjectModel
Imports System.Collections.Specialized
Public Class BindableTextBlock
Inherits TextBlock
Public Property InlineList As ObservableCollection(Of Inline)
Get
Return GetValue(InlineListProperty)
End Get
Set(ByVal value As ObservableCollection(Of Inline))
SetValue(InlineListProperty, value)
End Set
End Property
Public Shared ReadOnly InlineListProperty As DependencyProperty = _
DependencyProperty.Register("InlineList", _
GetType(ObservableCollection(Of Inline)), GetType(BindableTextBlock), _
New UIPropertyMetadata(Nothing, AddressOf OnInlineListPropertyChanged))
Private Shared Sub OnInlineListPropertyChanged(sender As DependencyObject, e As DependencyPropertyChangedEventArgs)
Dim textBlock As BindableTextBlock = TryCast(sender, BindableTextBlock)
Dim list As ObservableCollection(Of Inline) = TryCast(e.NewValue, ObservableCollection(Of Inline))
If textBlock IsNot Nothing Then
If list IsNot Nothing Then
' Add in the event handler for collection changed
AddHandler list.CollectionChanged, AddressOf textBlock.InlineCollectionChanged
textBlock.Inlines.Clear()
textBlock.Inlines.AddRange(list)
Else
textBlock.Inlines.Clear()
End If
End If
End Sub
''' <summary>
''' Adds the items to the inlines
''' </summary>
''' <param name="sender"></param>
''' <param name="e"></param>
''' <remarks></remarks>
Private Sub InlineCollectionChanged(sender As Object, e As NotifyCollectionChangedEventArgs)
Select Case e.Action
Case NotifyCollectionChangedAction.Add
Me.Inlines.AddRange(e.NewItems)
Case NotifyCollectionChangedAction.Reset
Me.Inlines.Clear()
Case NotifyCollectionChangedAction.Remove
For Each Line As Inline In e.OldItems
If Me.Inlines.Contains(Line) Then
Me.Inlines.Remove(Line)
End If
Next
End Select
End Sub
End Class
I think you may need some additional code on the PropertyChanged handler, so to initialise the textBlock.Inlines if the bound collection already has content, and to clear any existing context.
Everyone given good solutions, but I had a similar problem and after hours looking for solutions I decide try directly bind to default content. Without Dependency Properties.
Sorry my obsolete english... hehehehe
[ContentProperty("Inlines")]
public partial class WindowControl : UserControl
{
public InlineCollection Inlines { get => txbTitle.Inlines; }
}
Ok, lets use this on your xaml file...
<local:WindowControl>
.:: Register Logbook : Connected User - <Run Text="{Binding ConnectedUser.Name}"/> ::.
</local:WindowControl>
And voila!
It's because they bind inlines is unnecessary, you can modify de parts of a text from another control contents without a binding, this solution help-me.
Related
I want to find a way to implement different forms of modals in MVVM WPF application. Like dialogs with returning results, message boxes or modal sub-windows with some controls inside.
Can you give me an advice about an efficient and modern approach for it?
I rarely find much use for anything other than a confirmation request. "Do you really want to delete that?" kind of thing.
Things popping up and asking for extra input just aren't super useful in my experience.
To my mind though, what you're doing is splitting your code. There is code up to showing the dialog. There is then code happens if the user clicks OK or Yes or selects a thingummajig in the dialog.
I split these into separate pieces of code. So there is not necessarilly a need to stop code running. It's in a separate method ( or command ) which is only run if the user hits the right button.
My first approach uses a control which itself has no UI. It exists just to get something encapsulated into the view.
namespace UserInput
{
public class ConfirmationRequestor : Control, ICommandSource
{
public bool? ShowConfirmDialog
{
get
{
return (bool?)GetValue(ShowConfirmDialogProperty);
}
set
{
SetValue(ShowConfirmDialogProperty, value);
}
}
public static readonly DependencyProperty ShowConfirmDialogProperty =
DependencyProperty.Register("ShowConfirmDialog",
typeof(bool?),
typeof(ConfirmationRequestor),
new FrameworkPropertyMetadata(null
, new PropertyChangedCallback(ConfirmDialogChanged)
)
{ BindsTwoWayByDefault = true }
);
private static void ConfirmDialogChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if ((bool?)e.NewValue != true)
{
return;
}
ConfirmationRequestor cr = (ConfirmationRequestor)d;
Window parent = Window.GetWindow(cr) as Window;
MessageBoxResult result = MessageBox.Show(parent, cr.Message, cr.Caption, cr.MsgBoxButton, cr.MsgBoxImage);
if (result == MessageBoxResult.OK || result == MessageBoxResult.Yes)
{
if (cr.Command != null)
{
cr.Command.Execute(cr.CommandParameter);
}
}
cr.SetValue(ShowConfirmDialogProperty, false);
}
public MessageBoxButton MsgBoxButton
{
get { return (MessageBoxButton)GetValue(MsgBoxButtonProperty); }
set { SetValue(MsgBoxButtonProperty, value); }
}
public static readonly DependencyProperty MsgBoxButtonProperty =
DependencyProperty.Register("MsgBoxButton",
typeof(MessageBoxButton),
typeof(ConfirmationRequestor),
new PropertyMetadata(MessageBoxButton.OK));
public MessageBoxImage MsgBoxImage
{
get { return (MessageBoxImage)GetValue(MsgBoxImageProperty); }
set { SetValue(MsgBoxImageProperty, value); }
}
public static readonly DependencyProperty MsgBoxImageProperty =
DependencyProperty.Register("MsgBoxImage",
typeof(MessageBoxImage),
typeof(ConfirmationRequestor),
new PropertyMetadata(MessageBoxImage.Warning));
public string Caption
{
get { return (string)GetValue(CaptionProperty); }
set { SetValue(CaptionProperty, value); }
}
public static readonly DependencyProperty CaptionProperty =
DependencyProperty.Register("Caption",
typeof(string),
typeof(ConfirmationRequestor),
new PropertyMetadata(string.Empty));
public string Message
{
get { return (string)GetValue(MessageProperty); }
set { SetValue(MessageProperty, value); }
}
public static readonly DependencyProperty MessageProperty =
DependencyProperty.Register("Message",
typeof(string),
typeof(ConfirmationRequestor),
new PropertyMetadata(string.Empty));
public ICommand Command
{
get { return (ICommand)GetValue(CommandProperty); }
set { SetValue(CommandProperty, value); }
}
public static readonly DependencyProperty CommandProperty =
DependencyProperty.Register("Command", typeof(ICommand), typeof(ConfirmationRequestor), new UIPropertyMetadata(null));
public object CommandParameter
{
get { return (object)GetValue(CommandParameterProperty); }
set { SetValue(CommandParameterProperty, value); }
}
public static readonly DependencyProperty CommandParameterProperty =
DependencyProperty.Register("CommandParameter", typeof(object), typeof(ConfirmationRequestor), new UIPropertyMetadata(null));
public IInputElement CommandTarget
{
get { return (IInputElement)GetValue(CommandTargetProperty); }
set { SetValue(CommandTargetProperty, value); }
}
public static readonly DependencyProperty CommandTargetProperty =
DependencyProperty.Register("CommandTarget", typeof(IInputElement), typeof(ConfirmationRequestor), new UIPropertyMetadata(null));
}
}
I have a viewmodel designed to go with this which is exposed as a property on my window viewmodel. That viewmodel and the control encapsulate the confirmer functionality.
namespace UserInput
{
public class ConfirmationRequestorVM : INotifyPropertyChanged
{
private string caption;
public string Caption
{
get { return caption; }
set
{
caption = value;
NotifyPropertyChanged();
}
}
private string message;
public string Message
{
get { return message; }
set
{
message = value;
NotifyPropertyChanged();
}
}
private MessageBoxButton msgBoxButton;
public MessageBoxButton MsgBoxButton
{
get { return msgBoxButton; }
set
{
msgBoxButton = value;
NotifyPropertyChanged();
}
}
private MessageBoxImage msgBoxImage;
public MessageBoxImage MsgBoxImage
{
get { return msgBoxImage; }
set
{
msgBoxImage = value;
NotifyPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
}
Usage
In the relevant view
<input:ConfirmationRequestor
ShowConfirmDialog="{Binding ShowConfirmation, Mode=TwoWay}"
MsgBoxImage="{Binding confirmer.MsgBoxImage}"
MsgBoxButton="{Binding confirmer.MsgBoxButton}"
Message="{Binding confirmer.Message}"
Caption="{Binding confirmer.Caption}"
Command="{Binding OkCommand}"
/>
Most of that is fairly self explanatory.
When ShowConfirmation is set to true, that control will show a messagebox.
If the user clicks OK or Yes then the OkCommand will be executed.
Hence if you need deletion confirmation then you show your messagebox and the actual deletion would be in that OkCommand.
If you want more complicated UI then rather than using a messagebox at all you could show a window from similar control.
Let's call this a DialogueController. This could be rather simpler since we can rely on our own UI and bind commands.
This control would have a dependency property for view Type. This being the type of a usercontrol which needs to be shown.
Another bool dependency property and change handler would control showing the type.
When the showdialog bool becomes true.
A method instantiates a window ( or it could be a popup if you prefer ) instantiates a usercontrol of the type specified in our other DP. Sets the window datacontext to our current datacontext.
You could use getwindow to find the parent window and set that to parent of our new (dialogue) window instance.
Thus sharing the parent window viewmodel as datacontext.
Call showdialog on the window. Or you could just call show.
You then have whatever UI you wrote in your usercontrol shown in a window.
It has access to your parent window viewmodel so it can reference any of your data you need.
And... It's Yes or OK button can bind to whatever command you defined in that parent window viewmodel.
You could also do things like select from a list and bind selectedFoo in your parent window viewmodel as well.
If you don't showmodal then when you change shared properties anything bound in your parent window can get those changes.
Like I said though.
I've not really come across much demand for that sort of thing.
There are also some elephants in the not-dialog room.
A pop up referencing it's parent viewmodel.
And
An expander similarly.
And
Just overlaying a panel on top of everything inside your parent window. This is how I have done editing for data in datagrids in a number of apps.
The key thing I found was the realisation:
You can "just" split your code into code before the dialog. Show the dialog. Then the "doing" aspect of any dialog-like-UI can go in a separate command.
OK, it's not exactly a road to damascus moment. But it simplifies things. I like simple. More likely to work.
I am looking to find a generic way to support keyboard wedge scanning for my WPF TextBox controls.
(I am really a novice when it comes to more advanced WPF features, so I would like to ask if I am going in the right direction before I put a lot of time into research.)
What I am wanting to do is to add an Attached Property (or something) to my TextBoxes that will cause it to read all input into the box and then call a custom "ScanCompleted" command with the scanned input.
If an Attached Property is not a good fit for this, then is there a way to get this command on a TextBox without descending my own custom "ScanableTextBox"?
(Note: The criteria for a scan (instead of typed data) is that it will start with the Pause key (#19) and end with a Return key (#13).)
I think this could probably be accomplished with attached properties (behaviors), but would be much simpler and more straightforward to simply subclass TextBox and override the OnTextChanged, OnKeyDown, OnKeyUp and similar methods to add custom functionality.
Why don't you want to create your own control in this way?
update: Attached Behaviour
If you really don't want a derived control, here is an attached behaviour that accomplishes this (explanation below):
public class ScanReading
{
private static readonly IDictionary<TextBox, ScanInfo> TrackedTextBoxes = new Dictionary<TextBox, ScanInfo>();
public static readonly DependencyProperty ScanCompletedCommandProperty =
DependencyProperty.RegisterAttached("ScanCompletedCommand", typeof (ICommand), typeof (ScanReading),
new PropertyMetadata(default(ICommand), OnScanCompletedCommandChanged));
public static void SetScanCompletedCommand(TextBox textBox, ICommand value)
{
textBox.SetValue(ScanCompletedCommandProperty, value);
}
public static ICommand GetScanCompletedCommand(TextBox textBox)
{
return (ICommand) textBox.GetValue(ScanCompletedCommandProperty);
}
private static void OnScanCompletedCommandChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var textBox = d as TextBox;
if (textBox == null)
return;
var command = (ICommand) e.NewValue;
if (command == null)
{
textBox.Unloaded -= OnTextBoxUnloaded;
textBox.KeyUp -= OnTextBoxKeyUp;
TrackedTextBoxes.Remove(textBox);
}
else
{
textBox.Unloaded += OnTextBoxUnloaded;
TrackedTextBoxes.Add(textBox, new ScanInfo(command));
textBox.KeyUp += OnTextBoxKeyUp;
}
}
static void OnTextBoxKeyUp(object sender, KeyEventArgs e)
{
var textBox = (TextBox) sender;
var scanInfo = TrackedTextBoxes[textBox];
if (scanInfo.IsTracking)
{
if (e.Key == Key.Return)
{
scanInfo.ScanCompletedCommand.Execute(textBox.Text);
scanInfo.IsTracking = false;
}
}
else if (string.IsNullOrEmpty(textBox.Text) && e.Key == Key.Pause)
{
TrackedTextBoxes[textBox].IsTracking = true;
}
}
static void OnTextBoxUnloaded(object sender, RoutedEventArgs e)
{
var textBox = (TextBox) sender;
textBox.KeyUp -= OnTextBoxKeyUp;
textBox.Unloaded -= OnTextBoxUnloaded;
TrackedTextBoxes.Remove(textBox);
}
}
public class ScanInfo
{
public ScanInfo(ICommand scanCompletedCommand)
{
ScanCompletedCommand = scanCompletedCommand;
}
public bool IsTracking { get; set; }
public ICommand ScanCompletedCommand { get; private set; }
}
Consume this by declaring a TextBox like so (where local is the namespace of your attached property, and ScanCompleted is an ICommand on your view-model):
<TextBox local:ScanReading.ScanCompletedCommand="{Binding ScanCompleted}" />
Now when this property is set, we add the TextBox to a static collection along with its associated ICommand.
Each time a key is pressed, we check whether it is the Pause key. If it is, and if the TextBox is empty, we set a flag to true to start looking for the Enter key.
Now each time a key is pressed, we check whether it is the Enter key. If it is, we execute the command, passing in the TextBox.Text value, and reset the flag to false for that TextBox.
We've also added a handler for the TextBox.Unloaded event to clean up our event subscriptions and remove the TextBox from the static list.
My goal is to create a custom TextBlock control that has a new dependency property, SearchText. This property will contain a regular expression. All occurrences of this regular expression in the text of the TextBlock will be highlighted using a custom style (another DP).
My current implementation involves clearing all of the Inline objects in the TextBlock's InlineCollection. I then fill the TextBlock with runs for unhighlighted text and runs for highlighted text with the style applied (this method does not support adding inlines directly to the TextBlock, instead TextBlock.TextProperty has to be used).
Works great, but sometimes I get a strange exception when trying to clear the Inlines: InvalidOperationException: "Cannot modify the logical children for this node at this time because a tree walk is in progress."
This problem seems to be related to this one. I am modifying the inlines in the TextChanged function, but I'm using a flag to avoid infinite recursive edits.
Any thoughts on how to architect this custom control? Is there a better way to do this? How do I get around this exception?
Thanks!
In my implementation, I solved this by just adding another dependency property, called OriginalText. When it's modified, I updated both the Text property and update the highlighting. Here's the code:
public class HighlightTextBlock : TextBlock
{
public string HighlightedText
{
get { return (string)GetValue(HighlightedTextProperty); }
set { SetValue(HighlightedTextProperty, value); }
}
public static readonly DependencyProperty HighlightedTextProperty =
DependencyProperty.Register("HighlightedText", typeof(string), typeof(HighlightTextBlock), new UIPropertyMetadata(string.Empty, UpdateHighlightEffect));
public static readonly DependencyProperty OriginalTextProperty = DependencyProperty.Register(
"OriginalText", typeof(string), typeof(HighlightTextBlock), new PropertyMetadata(default(string), OnOriginalTextChanged));
private static void OnOriginalTextChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
var block = ((HighlightTextBlock)obj);
block.Text = block.OriginalText;
block.UpdateHighlightEffect();
}
public string OriginalText
{
get { return (string)GetValue(OriginalTextProperty); }
set { SetValue(OriginalTextProperty, value); }
}
private static void UpdateHighlightEffect(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
if (!(string.IsNullOrEmpty(e.NewValue as string) && string.IsNullOrEmpty(e.OldValue as string)))
((HighlightTextBlock)sender).UpdateHighlightEffect();
}
private void UpdateHighlightEffect()
{
if (string.IsNullOrEmpty(HighlightedText)) return;
var allText = GetCompleteText();
Inlines.Clear();
var indexOfHighlightString = allText.IndexOf(HighlightedText, StringComparison.InvariantCultureIgnoreCase);
if (indexOfHighlightString < 0)
{
Inlines.Add(allText);
}
else
{
Inlines.Add(allText.Substring(0, indexOfHighlightString));
Inlines.Add(new Run()
{
Text = allText.Substring(indexOfHighlightString, HighlightedText.Length),
Background = Consts.SearchHighlightColor,
});
Inlines.Add(allText.Substring(indexOfHighlightString + HighlightedText.Length));
}
}
private string GetCompleteText()
{
var allText = Inlines.OfType<Run>().Aggregate(new StringBuilder(), (sb, run) => sb.Append(run.Text), sb => sb.ToString());
return allText;
}
}
Still not sure if there's a better way to do this altogether, but I appear to have found a work around.
I was updating the inlines/runs in a function that was fired by the change notification for the TextProperty and the SearchTextProperty.
Now I'm firing the highlight/update code from a Dispatcher.BeginInvoke() call in the change notification with DispatcherPriority.Normal.
In case anyone wants an example of how to do this, I found this
public class RichTextBoxExtended : RichTextBox
{
static RichTextBoxExtended()
{
//DefaultStyleKeyProperty.OverrideMetadata(typeof(RichTextBoxExtended), new FrameworkPropertyMetadata(typeof(RichTextBoxExtended)));
}
public byte[] Text
{
get { return (byte[])GetValue(TextProperty); }
set { SetValue(TextProperty, value); }
}
// Using a DependencyProperty as the backing store for Text. This enables animation, styling, binding, etc...
public static readonly DependencyProperty TextProperty =
DependencyProperty.Register("Text", typeof(byte[]), typeof(RichTextBoxExtended), new UIPropertyMetadata(null, new PropertyChangedCallback(TextChangedCallback)));
private static void TextChangedCallback(DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
RichTextBoxExtended richTextBoxExtended = obj as RichTextBoxExtended;
richTextBoxExtended.ChangeText(e);
}
private void ChangeText(DependencyPropertyChangedEventArgs e)
{
//clear out any formatting properties
TextRange range = new TextRange(base.Document.ContentStart, base.Document.ContentEnd);
range.ClearAllProperties();
//load bytes into stream, load stream into range
MemoryStream stream = new MemoryStream(e.NewValue as byte[]);
range.Load(stream, DataFormats.Rtf);
}
}
Above is the custom control...
XAML implementing custom control...
<Grid>
<controls:RichTextBoxExtended x:Name="Document" Text="{Binding Path=File}">
</controls:RichTextBoxExtended>
</Grid>
Associated VM...
public class FileViewerViewModel : AViewModel
{
private byte[] _file = null;
public FileViewerViewModel(ILoggerFacade logger)
{
}
/// <summary>
/// Gets or sets the <seealso cref="DataFormats.Rtf"/> file representation as a <seealso cref="byte[]"/>
/// </summary>
public byte[] File
{
get
{
return _file;
}
set
{
_file = value;
RaiseChanged(() => this.File);
}
}
}
Finally..if I call...
FileViewerView view = _container.Resolve<FileViewerView>();
It fails.
Resolution of the dependency failed, type = "cyos.infrastructure.Views.FileViewerView", name = "". Exception message is: The current build operation (build key Build Key[cyos.infrastructure.Views.FileViewerView, null]) failed: Object reference not set to an instance of an object. (Strategy type BuildPlanStrategy, index 3)
If I remove the binding from within the XAML...
<Grid>
<controls:RichTextBoxExtended x:Name="Document">
</controls:RichTextBoxExtended>
</Grid>
Everything works without a hitch...no problems at all...ideas?
EDIT:
Switched to creating a new instance, bypassing Unity. Issue is still in the same place, exception at construction of FileViewerView at InitializeComponent() with "Object reference not set to an instance of an object."
at System.Windows.Markup.BamlRecordReader.ProvideValueFromMarkupExtension(MarkupExtension markupExtension, Object obj, Object member)
at System.Windows.Markup.BamlRecordReader.ReadPropertyArrayEndRecord()
at System.Windows.Markup.BamlRecordReader.ReadRecord(BamlRecord bamlRecord)
at System.Windows.Markup.BamlRecordReader.Read(Boolean singleRecord)
at System.Windows.Markup.TreeBuilderBamlTranslator.ParseFragment()
at System.Windows.Markup.TreeBuilder.Parse()
at System.Windows.Markup.XamlReader.LoadBaml(Stream stream, ParserContext parserContext, Object parent, Boolean closeStream)
at System.Windows.Application.LoadComponent(Object component, Uri resourceLocator)
at cyos.infrastructure.Views.FileViewerView.InitializeComponent() in c:\Documents and Settings\amciver\My Documents\dev\cyos\cyos\cyos.infrastructure\Views\FileViewerView.xaml:line 1
at cyos.infrastructure.Views.FileViewerView..ctor(FileViewerViewModel viewModel) in now...
I don't know what the problem with array, but List<byte> works.
I have a problem with the following scenario (code cut for brevity). Basically the Setter of my User Control Property isn't being called when the dependency property is set and I need to get around this.
I have the following code in my View.xaml
<Filter:Filter x:Name="ProductFilter" PrimaryItemSource="{Binding CarrierProducts}" />
In the View.xaml.cs
public ProductPricing()
{
InitializeComponent();
ViewModel.Filter.ProductPricing vm = new ViewModel.Filter.ProductPricing();
this.DataContext = vm;
}
In my ViewModel I expose a property
public ObservableCollection<Model.FilterItem> _carrierProducts;
public ObservableCollection<Model.FilterItem> CarrierProducts
{
get
{
return _carrierProducts;
}
set
{
if (_carrierProducts != value)
{
_carrierProducts = value;
RaisePropertyChanged("CarrierProducts");
}
}
}
Finally the Filter User control is defined like so.
public static readonly DependencyProperty PrimaryItemSourceProperty =
DependencyProperty.Register("PrimaryItemSource", typeof(ObservableCollection<Model.FilterItem>), typeof(Filter), new PropertyMetadata(null));
public ObservableCollection<Model.FilterItem> PrimaryItemSource
{
get
{
return (ObservableCollection<Model.FilterItem>)GetValue(PrimaryItemSourceProperty);
}
set
{
SetValue(PrimaryItemSourceProperty, value);
ComboBox combo = _filters.ElementAt(0);
FilterSourceChange(combo, value);
}
}
For some reason the PrimaryItemSource property is set but the Setter doesn't get called. Do I have to add a PropertyChange event to the PropertyMetadata object to handle this as that seems like a lot of code for something simple.
This is how a Dependency property that requires additional code to be run on set should be written:-
public ObservableCollection<Model.FilterItem> PrimaryItemSource
{
get { return (ObservableCollection<Model.FilterItem>)GetValue(PrimaryItemSourceProperty); }
set { SetValue(PrimaryItemSourceProperty , value); }
}
public static readonly DependencyProperty PrimaryItemSourceProperty =
DependencyProperty.Register(
"PrimaryItemSource",
typeof(ObservableCollection<Model.FilterItem>),
typeof(Filter), new PropertyMetadata(null, OnPrimaryItemSourceChanged));
private static void OnPrimaryItemSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
Filter filter = (Filter)d;
var oldValue = (ObservableCollection<Model.FilterItem>)e.OldValue;
var newValue = (ObservableCollection<Model.FilterItem>)e.NewValue;
filter.OnPrimaryItemSourceChanged(oldValue, newValue);
}
protected virtual void OnPrimaryItemSourceChanged(
ObservableCollection<Model.FilterItem> oldValue,
ObservableCollection<Model.FilterItem> newValue)
{
ComboBox combo = _filters.ElementAt(0);
FilterSourceChange(combo, newValue);
}
You use place a static DependencyPropertyChanged handler in the class that will cast down the dependency object to the correct type and then call an instance method to alert that instance of the change.
This change handler will get called whenever the underlying dependency property is changed be that via the SetValue call in the property Set method or by binding or any other means.
Yes, always use the callback if you need additional logic for the setter. This is a must in Silverlight and WPF.
As far as I know, the Setter would only be called when actually used from code. When you do Binding, things happen using the DependencyProperty framework.
You should also wrap your ComboBox combo = ... code into a this.Dispatcher.BeginInvoke(() => ... );, because that ensures the visual tree is initialized.
The Last parameter of the DependencyProperty.Register() method takes a PropertyMetaData where you're passing null. One of the overloads of the constructor takes a PropertyChangedCallback. Use this overload to define a callback function that will be called when your property is modified.
static void PropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
Filter filter = d as Filter;
ComboBox combo = filter._filters.ElementAt(0);
filter.FilterSourceChange(combo, filter.PrimaryItemSource);
}