Long ComboBox Text, Can't be Displayed Fully - winforms

As the image shown, the combo box text in my case is very long.
Without changing the width of the combo box, is there any method to show full text when the user hovers on it?

Without changing the width of the combo box, is there any method to show full text when the user hovers on it?
You can use the ToolTip component to show the items with long text when the mouse pointer hovers over the control. Whenever the SelectedIndex changes, measure the selected item by the TextRenderer.MeasureText method, and if the string's width is greater than the editbox's, set the long string as a tooltip for the control.
Note: The width of the EditBox = ComboBox.Width - ButtonWidth. You can get the ButtonWidth from the SystemInformation.VerticalScrollBarWidth property.
To apply this feature, let's create a little class and inherit the ComboBox control.
C#
// +
// using System.ComponentModel;
[DesignerCategory("Code")]
public class ComboBoxEx : ComboBox
{
private readonly ToolTip ttp;
public ComboBoxEx() : base() => ttp = new ToolTip();
private bool _showItemToolTip = true;
[DefaultValue(true)]
public bool ShowItemToolTip
{
get => _showItemToolTip;
set
{
if (_showItemToolTip != value)
{
_showItemToolTip = value;
ttp.SetToolTip(this, string.Empty);
}
}
}
protected override void OnSelectedIndexChanged(EventArgs e)
{
base.OnSelectedIndexChanged(e);
if (ShowItemToolTip)
{
string str = string.Empty;
if (SelectedIndex >= 0)
{
var sz = TextRenderer.MeasureText(Text, Font);
if (sz.Width > Width - SystemInformation.VerticalScrollBarWidth)
str = Text;
}
ttp.SetToolTip(this, str);
}
}
protected override void Dispose(bool disposing)
{
if (disposing) ttp.Dispose();
base.Dispose(disposing);
}
}
VB.Net
' +
' Imports System.ComponentModel
<DesignerCategory("Code")>
Public Class ComboBoxEx
Inherits ComboBox
Private ReadOnly ttp As ToolTip
Sub New()
MyBase.New
ttp = New ToolTip()
End Sub
Private _showItemToolTip As Boolean = True
<DefaultValue(True)>
Public Property ShowItemToolTip As Boolean
Get
Return _showItemToolTip
End Get
Set(value As Boolean)
If (_showItemToolTip <> value) Then
_showItemToolTip = value
ttp.SetToolTip(Me, String.Empty)
End If
End Set
End Property
Protected Overrides Sub OnSelectedIndexChanged(e As EventArgs)
MyBase.OnSelectedIndexChanged(e)
If ShowItemToolTip Then
Dim str = ""
If SelectedIndex >= 0 Then
Dim sz = TextRenderer.MeasureText(Text, Font)
If sz.Width > Width - SystemInformation.VerticalScrollBarWidth Then
str = Text
End If
End If
ttp.SetToolTip(Me, str)
End If
End Sub
Protected Overrides Sub Dispose(disposing As Boolean)
If disposing Then ttp.Dispose()
MyBase.Dispose(disposing)
End Sub
End Class
You can use the same approach if you don't want to subclass. Drop a ToolTip component and handle the ComboBox.SelectedIndexChanged event as shown above.

Related

WPF - Remove mnemonic function from textbox

Just discovered what is causing a HTML editor to throw the toys out when using a certain letter on a keyboard...
On the same page there are textboxes that contain things like html page name, title, navigate URL, menu text.... If one of the textboxes contains text with an underscore (say 'Test_Page') then the letter 'P' will not function in the HTML editor. I'm guessing (and could be way off base here as I didn't think textbox.txt could do this unlike Label.content) that WPF is taking the text entry and using it as a mnemonic key.. I do know that setting RecognisesAccessKey to false might cure it, but can't find a way to add that property or access ContentPresenter...
This is the class that I use to create the control and ideally would like to set it here
Public Class TBx
Inherits TextBox
Public Shared IsNewRecordProperty As DependencyProperty = DependencyProperty.Register("IsNewRecord", GetType(Boolean), GetType(TBx), New PropertyMetadata(New PropertyChangedCallback(AddressOf IsNewRecordChanged)))
Public Property IsNewRecord As Boolean
Get
Return GetValue(IsNewRecordProperty)
End Get
Set(value As Boolean)
SetValue(IsNewRecordProperty, value)
End Set
End Property
Protected Overrides Sub OnInitialized(e As System.EventArgs)
MyBase.OnInitialized(e)
VerticalAlignment = Windows.VerticalAlignment.Center
HorizontalAlignment = Windows.HorizontalAlignment.Left
BorderBrush = New SolidColorBrush(Colors.Silver)
Height = 22
SpellCheck.IsEnabled = True
UndoLimit = 0
If IsNewRecord = True Then
BorderThickness = New Thickness(1)
IsReadOnly = False
Background = New SolidColorBrush(Colors.White)
Else
BorderThickness = New Thickness(0)
IsReadOnly = True
Background = New SolidColorBrush(Colors.Transparent)
End If
End Sub
Private Shared Sub IsNewRecordChanged(sender As DependencyObject, e As DependencyPropertyChangedEventArgs)
Dim vControl As TBx = TryCast(sender, TBx)
Dim vBoolean As Boolean = e.NewValue
If vBoolean = True Then
vControl.BorderThickness = New Thickness(1)
vControl.IsReadOnly = False
vControl.Background = New SolidColorBrush(Colors.White)
Else
vControl.BorderThickness = New Thickness(0)
vControl.IsReadOnly = True
vControl.Background = New SolidColorBrush(Colors.Transparent)
End If
End Sub
End Class
Thank you
Couldn't find an easy way to do this from the class, but this works on the page
Dim CP As New ContentPresenter
CP.RecognizesAccessKey = False
Then add the TextBox to the ContentPresenter
Case 1
vLabel.Text = "Page Name"
With vTB
.Width = 200
.Name = vName & "PageNameTB"
.ToolTip = "This is the short name for the page"
.IsNewRecord = IsNewRecord
End With
CP.Content = vTB
The add the CP to the grid
RegisterControl(SecurePage_Grid, vTB)
Grid.SetColumn(vLabel, 0)
Grid.SetRow(vLabel, i)
If i = 1 Then
Grid.SetRow(CP, i)
Grid.SetColumn(CP, 1)
Else
Grid.SetRow(vTB, i)
Grid.SetColumn(vTB, 1)
End If
vGrid.Children.Add(vLabel)
If i = 1 Then
vGrid.Children.Add(CP)
Else
vGrid.Children.Add(vTB)
End If

change main form controls by trigger event inside canvas

I am currently working on a project that required me to use a canvas in order to draw rectangles around specific places in a picture (to mark places)
Each rectangle (actually "rectangle" since it is also a custom class that I created by inheriting from the Grid class and contain a rectangle object) contains properties and data about the marked place inside the picture.
my main form contains controls such as TextBox ,DropDownLists and etc.
Now what I am trying to do is that for each time I am clicking on the "rectangle" object the main form controls will be filled with the object data.
I do not have access to those controls from the canvas class.
this code is inside the costume canvas class to add the object into the canvas:
protected override void OnMouseLeftButtonDown( MouseButtonEventArgs e)
{
if(e.ClickCount==2)
{
testTi = new TiTest();
base.OnMouseLeftButtonDown(e);
startPoint = e.GetPosition(this);
testTi.MouseLeftButtonDown += testTi_MouseLeftButtonDown;
Canvas.SetLeft(testTi, e.GetPosition(this).X);
Canvas.SetTop(testTi, e.GetPosition(this).X);
this.Children.Add(testTi);
}
}
and by clicking an object that is placed inside the canvas i want to get the information.
for now just want to make sure i am getting the right object with a simple messagebox
void testTi_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
MessageBox.Show(sender.GetType().ToString());
}
this is my costume "Rectangle" class
class TiTest:Grid
{
private Label tiNameLabel;
private Rectangle tiRectangle;
private String SomeText = string.Empty;
private String version = "1.0";
private String application = "CRM";
private String CRID = "NNN";
public String SomeText1
{
get { return SomeText; }
set { SomeText = value; }
}
public Rectangle TiRectangle
{
get { return tiRectangle; }
set { tiRectangle = value; }
}
public Label TiNameLabel
{
get { return tiNameLabel; }
set { tiNameLabel = value; }
}
public TiTest()
{
this.SomeText = "Hello World!!";
this.TiNameLabel = new Label
{
Content = "Test Item",
VerticalAlignment = System.Windows.VerticalAlignment.Top,
HorizontalAlignment = System.Windows.HorizontalAlignment.Left
};
TiRectangle = new Rectangle
{
Stroke = Brushes.Red,
StrokeDashArray = new DoubleCollection() { 3 },//Brushes.LightBlue,
StrokeThickness = 2,
Cursor = Cursors.Hand,
Fill = new SolidColorBrush(Color.FromArgb(0, 0, 111, 0))
};
Background= Brushes.Aqua;
Opacity = 0.5;
this.Children.Add(this.tiNameLabel);
this.Children.Add(this.tiRectangle);
}
}
is there any way to access the main form controls from the costume canvas class or by the costume rectangle class?
Thanks in advance
You can have your main window be binded to a singletone ViewModel holding the properties of the rectangles.
ViewModel
public class MainWindowViewModel : INotifyPropertyChanged
{
#region Singletone
private static MainWindowViewModel _instance;
private MainWindowViewModel()
{
}
public static MainWindowViewModel Instance
{
get
{
if (_instance == null)
_instance = new MainWindowViewModel();
return _instance;
}
}
#endregion
#region Properties
private string _someInfo;
public string SomeInfo
{
get
{
return _someInfo;
}
set
{
if (_someInfo != value)
{
_someInfo = value;
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("SomeInfo"));
}
}
}
#endregion
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
#endregion
}
In main window xaml
<TextBox Text="{Binding SomeInfo}"/>
Also set the view model as your main window data context (in main window constructor for exmaple)
this.DataContext = MainWindowViewModel.Instance;
Finally, from where you handle the click event of the rectangles (testTi_MouseLeftButtonDown), access the MainWindowViewModel instance and set it's properties accordingly.
MainWindowViewModel.Instance.SomeInfo = myRectangle.SomeInfo;
This will trigger the PropertyChanged event, which will update your control's on the main window.
If you are not familiar with the MVVM (Model, View. View Model) pattern you can read about it here
Hope this helps

Quickest way to hide an array of pictureboxes

I have an array of pictureboxes named from B11 (co-ords 1,1) to B55 (co-ords 5,5). I would like to hide these all on startup (and in the middle of running). I was thinking of making an array of the names manually but would it be the best solution?
If they all have a common parent control, such as a panel or groupbox (or even the form):
Parent.SuspendLayout()
For Each pbox As PictureBox in Parent.Controls.OfType(Of PictureBox)()
pbox.Visible = False
Next pbox
Parent.ResumeLayout()
The Suspend/Resume-Layout() is to avoid flickering as you modify a bunch of controls at once.
You could extend the PictureBox class and use event handling to accomplish this by:
Adding a public property to the form to tell if the picture boxes should be shown or hidden.
Adding an event to the form that is raised when the show/hide picture box property is changed.
Extending the PictureBox class so that it subscribes to the event of the parent form.
Setting the visible property of the extended PictureBox class to the show/hide property of the parent form.
When the show/hide flag is changed on the parent form all of the picture boxes will change their visibility property accordingly.
Form Code:
public partial class PictureBoxForm : Form {
public PictureBoxForm() {
InitializeComponent();
this.pictureBoxesAdd();
}
private void pictureBoxesAdd() {
MyPictureBox mp1 = new MyPictureBox();
mp1.Location = new Point(1, 1);
MyPictureBox mp2 = new MyPictureBox();
mp2.Location = new Point(200, 1);
this.Controls.Add(mp1);
this.Controls.Add(mp2);
}
public event EventHandler PictureBoxShowFlagChanged;
public bool PictureBoxShowFlag {
get { return this.pictureBoxShowFlag; }
set {
if (this.pictureBoxShowFlag != value) {
pictureBoxShowFlag = value;
if (this.PictureBoxShowFlagChanged != null) {
this.PictureBoxShowFlagChanged(this, new EventArgs());
}
}
}
}
private bool pictureBoxShowFlag = true;
private void cmdFlip_Click( object sender, EventArgs e ) {
this.PictureBoxShowFlag = !this.PictureBoxShowFlag;
}
}
Extended PictureBox Code:
public class MyPictureBox : PictureBox {
public MyPictureBox() : base() {
this.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
this.ParentChanged += new EventHandler(MyPictureBox_ParentChanged);
}
private void MyPictureBox_ParentChanged( object sender, EventArgs e ) {
try {
PictureBoxForm pbf = (PictureBoxForm)this.Parent;
this.Visible = pbf.PictureBoxShowFlag;
pbf.PictureBoxShowFlagChanged += new
EventHandler(pbf_PictureBoxShowFlagChanged);
} catch { }
}
private void pbf_PictureBoxShowFlagChanged( object sender, EventArgs e ) {
PictureBoxForm pbf = (PictureBoxForm)sender;
this.Visible = pbf.PictureBoxShowFlag;
}
}
...or just put 'em all on a Panel, and change the panel's visibility.

Code to check if a cell of a DataGrid is currently edited

Is there a simple possibility to check if the DataGrid is currently in EditMode (Without to subscribe to BeginningEdit and CellEditEnding)
It seems you can also get this information from the items view, namely this works:
IEditableCollectionView itemsView = stateGrid.Items;
if (itemsView.IsAddingNew || itemsView.IsEditingItem)
{
stateGrid.CommitEdit(DataGridEditingUnit.Row, true);
}
I have not confirmed this but most likely you could get these flags in a viewmodel if your bound collection provides an IEditableCollectionView.
Ok, I havent found a simple solution and no one pointed me to one. The following code can be used to add an attached property IsInEditMode to a DataGrid. Hope it helps someone:
public class DataGridIsInEditModeTracker {
public static bool GetIsInEditMode(DataGrid dataGrid) {
return (bool)dataGrid.GetValue(IsInEditModeProperty);
}
private static void SetIsInEditMode(DataGrid dataGrid, bool value) {
dataGrid.SetValue(IsInEditModePropertyKey, value);
}
private static readonly DependencyPropertyKey IsInEditModePropertyKey = DependencyProperty.RegisterAttachedReadOnly("IsInEditMode", typeof(bool), typeof(DataGridIsInEditModeTracker), new UIPropertyMetadata(false));
public static readonly DependencyProperty IsInEditModeProperty = IsInEditModePropertyKey.DependencyProperty;
public static bool GetProcessIsInEditMode(DataGrid dataGrid) {
return (bool)dataGrid.GetValue(ProcessIsInEditModeProperty);
}
public static void SetProcessIsInEditMode(DataGrid dataGrid, bool value) {
dataGrid.SetValue(ProcessIsInEditModeProperty, value);
}
public static readonly DependencyProperty ProcessIsInEditModeProperty =
DependencyProperty.RegisterAttached("ProcessIsInEditMode", typeof(bool), typeof(DataGridIsInEditModeTracker), new FrameworkPropertyMetadata(false, delegate(DependencyObject d,DependencyPropertyChangedEventArgs e) {
DataGrid dataGrid = d as DataGrid;
if (null == dataGrid) {
throw new InvalidOperationException("ProcessIsInEditMode can only be used with instances of the DataGrid-class");
}
if ((bool)e.NewValue) {
dataGrid.BeginningEdit += new EventHandler<DataGridBeginningEditEventArgs>(dataGrid_BeginningEdit);
dataGrid.CellEditEnding += new EventHandler<DataGridCellEditEndingEventArgs>(dataGrid_CellEditEnding);
} else {
dataGrid.BeginningEdit -= new EventHandler<DataGridBeginningEditEventArgs>(dataGrid_BeginningEdit);
dataGrid.CellEditEnding -= new EventHandler<DataGridCellEditEndingEventArgs>(dataGrid_CellEditEnding);
}
}));
static void dataGrid_CellEditEnding(object sender, DataGridCellEditEndingEventArgs e) {
SetIsInEditMode((DataGrid)sender,false);
}
static void dataGrid_BeginningEdit(object sender, DataGridBeginningEditEventArgs e) {
SetIsInEditMode((DataGrid)sender, true);
}
}
To use it, set on the datagrid the ProcessIsInEditMode- property to true:
<DataGrid local:DataGridIsInEditModeTracker.ProcessIsInEditMode="True" .. other properties ..>
Afer that you will have the IsInEditMode-property in sync with the mode of the DataGrid.
If you want also the editing cell, change the code in BeginningEdit accoringly.
I found a shorter workaround (VB.NET/C#):
VB.NET
<Extension>
Public Function GetContainerFromIndex(Of TContainer As DependencyObject) _
(ByVal itemsControl As ItemsControl, ByVal index As Integer) As TContainer
Return DirectCast(
itemsControl.ItemContainerGenerator.ContainerFromIndex(index), TContainer)
End Function
<Extension>
Public Function IsEditing(ByVal dataGrid As DataGrid) As Boolean
Return dataGrid.GetEditingRow IsNot Nothing
End Function
<Extension>
Public Function GetEditingRow(ByVal dataGrid As DataGrid) As DataGridRow
Dim sIndex = dataGrid.SelectedIndex
If sIndex >= 0 Then
Dim selected = dataGrid.GetContainerFromIndex(Of DataGridRow)(sIndex)
If selected.IsEditing Then Return selected
End If
For i = 0 To dataGrid.Items.Count - 1
If i = sIndex Then Continue For
Dim item = dataGrid.GetContainerFromIndex(Of DataGridRow)(i)
If item.IsEditing Then Return item
Next
Return Nothing
End Function
C#:
public static TContainer GetContainerFromIndex<TContainer>
(this ItemsControl itemsControl, int index)
where TContainer : DependencyObject
{
return (TContainer)
itemsControl.ItemContainerGenerator.ContainerFromIndex(index);
}
public static bool IsEditing(this DataGrid dataGrid)
{
return dataGrid.GetEditingRow() != null;
}
public static DataGridRow GetEditingRow(this DataGrid dataGrid)
{
var sIndex = dataGrid.SelectedIndex;
if (sIndex >= 0)
{
var selected = dataGrid.GetContainerFromIndex<DataGridRow>(sIndex);
if (selected.IsEditing) return selected;
}
for (int i = 0; i < dataGrid.Items.Count; i++)
{
if (i == sIndex) continue;
var item = dataGrid.GetContainerFromIndex<DataGridRow>(i);
if (item.IsEditing) return item;
}
return null;
}
All the answers above using IsEditing on the datagridrow or IsEdititngItem on the IEditableCollectionView are partial answers to me :
If the user enter edition, then clics on any other cell, the EndEdit event is fired but the DataGridRow has still the property IsEditing to True !!! And if you try to find the DataGridCell responsible, its IsEditingProperty Is always false...
I think it's a bug. And to have the desired behaviour, I had to write this Ugly workaround
Public Shared ReadOnly ForceEndEditProp As DependencyProperty =
DependencyProperty.RegisterAttached("ForceEndEdit", GetType(Boolean),
GetType(DataGridEditing), New PropertyMetadata(False, AddressOf ForceEndEditChanged))
Protected Shared Sub ForceEndEditChanged(d As DependencyObject, e As DependencyPropertyChangedEventArgs)
Dim g As DataGrid = TryCast(d, DataGrid)
If g Is Nothing Then Return
''IsCommiting prevents a StackOverflow ...
Dim IsCommiting As Boolean = False
AddHandler g.CellEditEnding, Sub(s, e1)
If IsCommiting Then Return
IsCommiting = True
g.CommitEdit(DataGridEditingUnit.Row, True)
IsCommiting = False
End Sub
End Sub
Public Shared Function GetForceEndEdit(o As DependencyObject) As Boolean
Return o.GetValue(ForceEndEditProp)
End Function
Public Shared Sub SetForceEndEdit(ByVal o As DependencyObject, ByVal value As Boolean)
o.SetValue(ForceEndEditProp, value)
End Sub
This basicly force the grid to set IsEditing = false on the datagridrow, when any cell stops editing.

Data binding the TextBlock.Inlines

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 &#x2600."
/// </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.

Resources