User Control events when "splitting" a XAML file - wpf

Several people have asked how to split up large XAML files into smaller more manageable or readable chunks. In my case I have a XAML file with 10 tabs and each tab has a lot of complex controls. So the XAML file for this is huge and hard to read.
The "standard" answer for this seems to be User Controls.
I'm sure this is a real noob question, but if all you're trying to do is split up the XAML file, how do you do it without splitting up the C#, too? When you create a WPF user control Visual Studio creates a new XAML file plus new code-behind file to go with it and handle the events.
What I really wanted was the equivalent of a "C# partial" directive for XAML so I could just split it up among multiple files but have the events handled in one place. How close can I get to that?
Thanks in advance!

There is no way to do as you describe (to allow multiple xaml files to share the same c# code). Also from what you describe there isn't an easy way to quickly abstract that code without having to make some changes. Since wpf events are typically driven by commands, the best solution would probably be to change your events to fire commands rather than putting the logic within the actual event handler itself, then calling it from the user control would be trivial.
But before you go changing all your code, you may be able to abstract out a lot of the long stuff using styles, which are way easier to abstract out and shouldn't mess with your events. So if you notice repeating how things are setup across lots of controls, just declare all of it as a style and you can move it into a resource dictionary elsewhere to remove some clutter.
To elaborate a bit you can use styles not just to stop repetition, but also to abstract out how you define your controls (much like you are trying to use user controls for, you can also define events there). For example...
<Style TargetType="TabItem" x:Key="Tab1Style">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<StackPanel>
<!--Note even if this style is defined in a resource file the
events will still be tied to the class of the control
using the style-->
<Button Click="Button_Click"/>
<Button Click="Button_Click_1" />
<Button Click="Button_Click_2" />
</StackPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Then simplify one of your tabitems to simply...
<TabControl>
<TabItem Style="{StaticResource Tab1Style}" />
</TabControl>
If you really have your heart set on user controls you could also just route all the events out. Something like this...
<UserControl ...>
<Button Click="OnClick"/>
</UserControl>
public partial class UserControl1 : UserControl
{
public static readonly RoutedEvent ButtonClick = EventManager.RegisterRoutedEvent(
"ButtonClick", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(UserControl1));
public event RoutedEventHandler ButtonClickHandler
{
add { AddHandler(ButtonClick, value); }
remove { RemoveHandler(ButtonClick, value); }
}
private void OnClick(object sender, RoutedEventArgs e)
{
RoutedEventArgs newEventArgs = new RoutedEventArgs(UserControl1.ButtonClick);
RaiseEvent(newEventArgs);
}
public UserControl1()
{
InitializeComponent();
}
}
<Window>
<local:UserControl1 ButtonClickHandler="Button_Click" />
</Window>
private void Button_Click(object sender, RoutedEventArgs e)
{
MessageBox.Show("Click");
}
(like I said lots of plumbing code)

Related

How to do communication between an object and its clone?

I have an object and one clone of the same object. First one is added in one panel(canvas) as a child and the second one is added to another panel as a child. Now, if I am doing some operations(say delete) on the main object, what is the best way in which the clone object also get notified(deleted). Basically, I am looking for how to create a link between a object and it's clone?
Thanks
Given the details, if you want both to always be in sync then I would rather suggest to 'avoid the clone'. Make the two work on the same instance, unless you have better reason to maintain the clone.
If there is some set of properties that you want to sync, then consider moving those to a different class and make the two (original and clone) share that instance.
Something like this:
public class PartiallyCloneable{
public SharedValue Shared{get;set;}
public ClonedValue Cloned{get;set;}
public PartiallyCloneable Clone(){
//deep copy ClonedValue, return same instance of SharedValue
}
}
public class SharedValue{
}
public class ClonedValue{
}
There's no simple direct way to do that, and adding the same object to multiple parents will throw exception. So you need to write a few lines of code. My guess is your approach is somehow a difficult one and you need to rethink the problem. Let me give you some ideas:
If you're using MVVM pattern (or you want to choose the standard path), define a ViewModel (which should derive from DependencyObject) for your object and bind the DataContext of both objects to one instance of this ViewModel. So that both View objects reference the same ViewModel. Then use Commands to do stuff to either objects.
If you don't want to go that far, use the same style for object and its clone, and set event handlers inside that style.
<Style x:Key="myStyle" TargetType="Control">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<Border BorderThickness="1" BorderBrush="Black" Unloaded="objectUnloaded">
<StackPanel>
<TextBlock Text="something"/>
<Button Content="delete" Click="deleteButtonClicked"/>
<Button Content="otherStuff" Click="otherStuffButtonClicked"/>
</StackPanel>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Also bind the Tag of each object to other.
<Control x:Name="main" Style="{StaticResource myStyle}"
Tag="{Binding ElementName=clone}"/>
<Control x:Name="clone" Style="{StaticResource myStyle}"
Tag="{Binding ElementName=main}"/>
Now if you implement those event handlers in code behind, you can access both object and clone at the same time:
private void otherStuffButtonClicked(object sender, RoutedEventArgs e)
{
var obj = sender as FrameworkElement;
var other = (obj.TemplatedParent as FrameworkElement).Tag;
//do stuff to obj
//do stuff to other
}

WPF - Events on a ControlTemplate?

Does anyone know why I can't set an event on a control template??
For example, the following line of code will not compile. It does this with any events in a control template.
<ControlTemplate x:Key="DefaultTemplate" TargetType="ContentControl">
<StackPanel Loaded="StackPanel_Loaded">
</StackPanel>
</ControlTemplate>
I am using a MVVM design pattern and the control here is located in a ResourceDictionary that is added to the application's MergedDictionaries.
Does anyone know why I can't set an event on a control template??
Actually, you can... But where would you expect the event handler to be defined ? The ResourceDictionary has no code-behind, so there is no place to put the event handler code. You can, however, create a class for your resource dictionary, and associate it with the x:Class attribute :
<ResourceDictionary x:Class="MyNamespace.MyClass"
xmlns=...>
<ControlTemplate x:Key="DefaultTemplate" TargetType="ContentControl">
<StackPanel Loaded="StackPanel_Loaded">
</StackPanel>
</ControlTemplate>
C# code :
namespace MyNamespace
{
public partial class MyClass : ResourceDictionary
{
void StackPanel_Loaded(object sender, RoutedEventArgs e)
{
...
}
}
}
(you might also need to change the build action of the resource dictionary to "Page", I don't remember exactly...)

How to select ListBoxItem upon clicking on button in Template?

I have the following Data Template applied to a ListBox:
<DataTemplate x:Key="MyTemplate" DataType="{x:Type DAL:Person}">
<StackPanel Orientation="Horizontal">
<Button Content="X" Command="{x:Static cmd:MyCommands.Remove}"/>
<TextBlock Text="{Binding Person.FullName}" />
</StackPanel>
</DataTemplate>
When I click on the button the command gets fired but the ListBoxItem doesn't get selected. How do I force it to get selected, so that I can get the selected item in my "executed" method?
Thanks
A better way, since you're not really interested in selecting the item (because it will quickly get deleted anyway) would be to pass the item itself to the Command as a CommandParameter.
Alternatively, you can go about in a roundabout manner either with code-behind or with triggers, but I don't think it would be as to the point. For example:
you could handle the ButtonBase.Click event on your listbox, like
<ListBox ButtonBase.Click="lb_Click"
...
then in your code behind, do this:
private void lb_Click(object sender, RoutedEventArgs e)
{
object clicked = (e.OriginalSource as FrameworkElement).DataContext;
var lbi = lb.ItemContainerGenerator.ContainerFromItem(clicked) as ListBoxItem;
lbi.IsSelected = true;
}
That gets the clicked bound item, because the datacontext of the button is inherited from it's templated item, then the actual autogenerated ListBoxItem from the ListBox's ItemContainerGenerator, and sets the IsSelected property to true. I think that's one of the fastest and easiest ways. Also works with multiple ButtonBase-derived objects in the template.
Of course you can also more nicely encapsulate all this (more or less exactly the same) as a reusable Behavior:
public class SelectItemOnButtonClick : Behavior<ListBox>
{
protected override void OnAttached()
{
base.OnAttached();
this.AssociatedObject.AddHandler(ButtonBase.ClickEvent, new RoutedEventHandler(handler), true);
}
protected override void OnDetaching()
{
this.AssociatedObject.RemoveHandler(ButtonBase.ClickEvent, new RoutedEventHandler(handler));
base.OnDetaching();
}
private void handler(object s, RoutedEventArgs e)
{
object clicked = (e.OriginalSource as FrameworkElement).DataContext;
var lbi = AssociatedObject.ItemContainerGenerator.ContainerFromItem(clicked) as ListBoxItem;
lbi.IsSelected = true;
}
}
You can use it like this:
<ListBox xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity" ...>
<i:Interaction.Behaviors>
<local:SelectItemOnButtonClick />
</i:Interaction.Behaviors>
</ListBox>
Add error handling code like at least null checks, of course - wouldn't want a simple thing like this bombing your app.
To understand the problem, the button sets the Handled property to true for all the mouse events that act on it (MouseDown/Click) so they aren't being considered by the ListBoxItem. You could also attach the MouseDown event to the ListBox and walk the visual tree upwards until you reach the parent ListBoxItem but that's a lot more tricky... eh if you're curious, you can read this article to know why, basically you'll also encounter FrameworkContentElements (which also respond to MouseDown) so the code will get more complicated, with the upside that anything clicked inside the datatemplate will trigger the ListBoxItem to be selected, regardless of whether it marked the event as handled.
Heh, I also tried to do it exclusively with styles and triggers but it got ugly fast and I lost interest (and lost track of all the... err thingies). Basically it could be solved, I think, but I reaaaly don't think it's worth the bother. Maybe I overlooked something obvious though, don't know.
Make the underlying object expose a RemoveCommand property, and bind the button's Command property to it. This simplifies the data template; it also greatly simplifies the case where application logic may dictate that a specific item can't be removed.
Alex, thanks for answer. Your solution with Behavior is great. First solution is not so good because that will work only if you click on specific Button. Here is one more solution that will work on click on arbitrary control form ListBoxItem template:
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem"
BasedOn="{StaticResource {x:Type ListBoxItem}}">
<Style.Triggers>
<Trigger Property="IsKeyboardFocusWithin" Value="True">
<Setter Property="IsSelected" Value="True"/>
</Trigger>
</Style.Triggers>
</Style>
</ListBox.ItemContainerStyle>
That is XAML only approach. I also set BasedOn property just to be sure to not override the current ListBoxItem style.

How to build a generic/re-usable modal dialog for WPF following MVVM

I would like to build a generic/re-usable modal dialog that I can use in our WPF (MVVM) - WCF LOB application.
I have a Views and associated ViewModels that I would like to display using dialogs. Bindings between Views and ViewModels are done using Type-targeted DataTemplates.
Here are some requirements that I have been able to draft:
I prefer this to be based on a Window instead of using Adorners and controls that act like a modal dialog.
It should get its minimum size from the content.
It should center on the owner window.
The window must not show the Minimize and Maximize buttons.
It should get its title from the content.
What is the best way to do this?
I usually deal with this by injecting this interface into the appropriate ViewModels:
public interface IWindow
{
void Close();
IWindow CreateChild(object viewModel);
void Show();
bool? ShowDialog();
}
This allows the ViewModels to spaw child windows and show them modally on modeless.
A reusable implementation of IWindow is this:
public class WindowAdapter : IWindow
{
private readonly Window wpfWindow;
public WindowAdapter(Window wpfWindow)
{
if (wpfWindow == null)
{
throw new ArgumentNullException("window");
}
this.wpfWindow = wpfWindow;
}
#region IWindow Members
public virtual void Close()
{
this.wpfWindow.Close();
}
public virtual IWindow CreateChild(object viewModel)
{
var cw = new ContentWindow();
cw.Owner = this.wpfWindow;
cw.DataContext = viewModel;
WindowAdapter.ConfigureBehavior(cw);
return new WindowAdapter(cw);
}
public virtual void Show()
{
this.wpfWindow.Show();
}
public virtual bool? ShowDialog()
{
return this.wpfWindow.ShowDialog();
}
#endregion
protected Window WpfWindow
{
get { return this.wpfWindow; }
}
private static void ConfigureBehavior(ContentWindow cw)
{
cw.WindowStartupLocation = WindowStartupLocation.CenterOwner;
cw.CommandBindings.Add(new CommandBinding(PresentationCommands.Accept, (sender, e) => cw.DialogResult = true));
}
}
You can use this Window as a reusable host window. There's no code-behind:
<Window x:Class="Ploeh.Samples.ProductManagement.WpfClient.ContentWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:self="clr-namespace:Ploeh.Samples.ProductManagement.WpfClient"
xmlns:pm="clr-namespace:Ploeh.Samples.ProductManagement.PresentationLogic.Wpf;assembly=Ploeh.Samples.ProductManagement.PresentationLogic.Wpf"
Title="{Binding Path=Title}"
Height="300"
Width="300"
MinHeight="300"
MinWidth="300" >
<Window.Resources>
<DataTemplate DataType="{x:Type pm:ProductEditorViewModel}">
<self:ProductEditorControl />
</DataTemplate>
</Window.Resources>
<ContentControl Content="{Binding}" />
</Window>
You can read more about this (as well as download the full code sample) in my book.
I'm answering my own question to help others find all answers I struggled to find in one place. What above seems like a straight forward problem, actually presents multiple problems that I hope to answer sufficiently below.
Here goes.
Your WPF window that will serve as the generic dialog can look something like this:
<Window x:Class="Example.ModalDialogView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ex="clr-namespace:Example"
Title="{Binding Path=mDialogWindowTitle}"
ShowInTaskbar="False"
WindowStartupLocation="CenterOwner"
WindowStyle="SingleBorderWindow"
SizeToContent="WidthAndHeight"
ex:WindowCustomizer.CanMaximize="False"
ex:WindowCustomizer.CanMinimize="False"
>
<DockPanel Margin="3">
<StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" FlowDirection="RightToLeft">
<Button Content="Cancel" IsCancel="True" Margin="3"/>
<Button Content="OK" IsDefault="True" Margin="3" Click="Button_Click" />
</StackPanel>
<ContentPresenter Name="WindowContent" Content="{Binding}"/>
</DockPanel>
</Window>
Following MVVM, the right way to show a dialog is through a mediator. To use a mediator, you typically require some service locator as well. For mediator specific details, look here.
The solution I settled on involved implementing an IDialogService interface that is resolved through a simple static ServiceLocator. This excellent codeproject article has the details on that. Take note of this message in the article forum. This solution also solves the problem of discovering the owner window via the ViewModel instance.
Using this interface, you can call IDialogService.ShowDialog(ownerViewModel, dialogViewModel). For now, I'm calling this from the owner ViewModel, meaning I have hard references between my ViewModels. If you use aggregated events, you will probably call this from a conductor.
Setting the minimum size on the View that will eventually be displayed in the dialog doesn't automatically set the minimum size of the dialog. Also, since the logical tree in the dialog contains the ViewModel, you can't just bind to the WindowContent element's properties. This question has an answer with my solution.
The answer I mention above also includes code that centers the window on the owner.
Finally, disabling the minimize and maximize buttons is something WPF can't natively do. The most elegant solution IMHO is using this.

Clicking HyperLinks in a RichTextBox without holding down CTRL - WPF

I have a WPF RichTextBox with isReadOnly set to True. I would like users to be able to click on HyperLinks contained within the RichTextBox, without them having to hold down Ctrl.
The Click event on the HyperLink doesn't seem to fire unless Ctrl is held-down, so I'm unsure of how to proceed.
I found a solution. Set IsDocumentEnabled to "True" and set IsReadOnly to "True".
<RichTextBox IsReadOnly="True" IsDocumentEnabled="True" />
Once I did this, the mouse would turn into a 'hand' when I hover over a text displayed within a HyperLink tag. Clicking without holding control will fire the 'Click' event.
I am using WPF from .NET 4. I do not know if earlier versions of .NET do not function as I describe above.
JHubbard80's answer is a possible solution, it's the easiest way if you do not need the content to be selected.
However I need that :P here is my approach: set a style for the Hyperlinks inside the RichTextBox. The essential is to use a EventSetter to make the Hyperlinks handling the MouseLeftButtonDown event.
<RichTextBox>
<RichTextBox.Resources>
<Style TargetType="Hyperlink">
<Setter Property="Cursor" Value="Hand" />
<EventSetter Event="MouseLeftButtonDown" Handler="Hyperlink_MouseLeftButtonDown" />
</Style>
</RichTextBox.Resources>
</RichTextBox>
And in codebehind:
private void Hyperlink_MouseLeftButtonDown(object sender, MouseEventArgs e)
{
var hyperlink = (Hyperlink)sender;
Process.Start(hyperlink.NavigateUri.ToString());
}
Thanks to gcores for the inspiaration.
Managed to find a way around this, pretty much by accident.
The content that's loaded into my RichTextBox is just stored (or inputted) as a plain string. I have subclassed the RichTextBox to allow binding against it's Document property.
What's relevant to the question, is that I have an IValueConverter Convert() overload that looks something like this (code non-essential to the solution has been stripped out):
FlowDocument doc = new FlowDocument();
Paragraph graph = new Paragraph();
Hyperlink textLink = new Hyperlink(new Run(textSplit));
textLink.NavigateUri = new Uri(textSplit);
textLink.RequestNavigate +=
new System.Windows.Navigation.RequestNavigateEventHandler(navHandler);
graph.Inlines.Add(textLink);
graph.Inlines.Add(new Run(nonLinkStrings));
doc.Blocks.Add(graph);
return doc;
This gets me the behavior I want (shoving plain strings into RichTextBox and getting formatting) and it also results in links that behave like a normal link, rather than one that's embedded in a Word document.
My answer is based on #BionicCode's answer, which I wanted to extend with the event handler code, which I had some difficulties to get it working.
<RichTextBox IsDocumentEnabled="True" IsReadOnly="True">
<FlowDocument>
<Paragraph>
<Run Text="Some editable text" />
<Hyperlink x:Name="DuckduckgoHyperlink"
NavigateUri="https://duckduckgo.com">
DuckDuckGo
</Hyperlink>
</Paragraph>
</FlowDocument>
</RichTextBox>
I changed his code slightly:
I wanted the RichTextBox to be readonly. When the RichTextBox is readonly, it is not necessary to put the HyperLink into a TextBlock. However, using TextBlock in a RichTextBlock where the user can make changes is a great suggestion.
In my programming style, code related stuff belongs in the code behind file. Event handlers are code and I prefer to even add the event handler to its control from code behind. To do that, it is enough to give the Hyperlink a name.
Code behind
I needed to display some rich text with links in a HelpWindow:
public HelpWindow() {
InitializeComponent();
DuckduckgoHyperlink.RequestNavigate += Hyperlink_RequestNavigate;
}
private void Hyperlink_RequestNavigate(object sender,
RequestNavigateEventArgs e)
{
Process.Start(new ProcessStartInfo(e.Uri.AbsoluteUri) {
UseShellExecute = true,
});
e.Handled = true;
}
Note that the same event handler can be used by any HyperLink. Another solution would be not to define the URL in XAML but hard code it in the event handler, in which case each HyperLink needs its own event handler.
In various Stackoverflow answers I have seen the code:
Process.Start(new ProcessStartInfo(e.Uri.AbsoluteUri));
Which resulted in the error message:
System.ComponentModel.Win32Exception: 'An error occurred trying to start process 'https://duckduckgo.com/' with working directory '...\bin\Debug\net6.0-windows'. The system cannot find the file specified.'
Do not handle any mouse events explicitly and do not force the cursor explicitly - like suggested in every answer.
It's also not required to make the complete RichTextBox read-only (as suggested in another answer).
To make the Hyperlink clickable without pressing the Ctrl key, the Hyperlink must be made read-only e.g., by wrapping it into a TextBlock (or alternatively by making the complete RichTextBox read-only, of course).
Then simply handle the Hyperlink.RequestNavigate event or/and attach an ICommand to the Hyperlink.Command property:
<RichTextBox IsDocumentEnabled="True">
<FlowDocument>
<Paragraph>
<Run Text="Some editable text" />
<TextBlock>
<Hyperlink NavigateUri="https://duckduckgo.com"
RequestNavigate="OnHyperlinkRequestNavigate">
DuckDuckGo
</Hyperlink>
</TextBlock>
</Paragraph>
</FlowDocument>
</RichTextBox>
I changed EventSetter from #hillin's answer.
MouseLeftButtonDown didn't work in my code (.Net framework 4.5.2).
<EventSetter Event="RequestNavigate" Handler="Hyperlink_RequestNavigate" />
private void Hyperlink_RequestNavigate(object sender, System.Windows.Navigation.RequestNavigateEventArgs e)
{
Process.Start(e.Uri.ToString());
}
If you want to turn Arrow into a Hand cursor always without default system navigation, below is the approach.
<RichTextBox>
<RichTextBox.Resources>
<Style TargetType="{x:Type Hyperlink}">
<EventSetter Event="MouseEnter" Handler="Hyperlink_OnMouseEnter"/>
</Style>
</RichTextBox.Resources>
</RichTextBox>
private void Hyperlink_OnMouseEnter(object sender, MouseEventArgs e)
{
var hyperlink = (Hyperlink)sender;
hyperlink.ForceCursor = true;
hyperlink.Cursor = Cursors.Hand;
}

Resources