I have some UserControls in my project like pnlCanvas and pnlTools.
There are several buttons in pnlTools like "Add Circle", "Add Rectangle", "Add Text", ...
When the user clicks on one of the buttons, an element sould be added to the childrens of the Canvas (cnvsObjects) which is located in the pnlCanvas.
My MainWindow.xaml is like this:
<Window x:Class=...>
<Grid>
...
<local:pnlCanvas Grid.Column="2"/>
<GridSplitter Grid.Column="3" HorizontalAlignment="Stretch"/>
<local:pnlTools Grid.Column="4" />
...
</Grid>
</Window>
The pnlCanvas.xaml:
<UserControl x:Class=...>
<GroupBox>
<GroupBox.Header...>
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
<Canvas x:Name="cnvsObjects" Width="1920" Height=...>
</Canvas>
</ScrollViewer>
</GroupBox>
</UserControl>
The pnlTools.xaml:
<UserControl x:Class=...>
<GroupBox>
<GroupBox.Header...>
<StackPanel>
<Button Content="Add Text" Click="Button_Click"></Button>
<Button Content="Add Rectangle"></Button>
<Button Content="Add Line"></Button>
...
</StackPanel>
</GroupBox>
</UserControl>
The pnlTools.xaml.cs:
....
public partial class pnlTools : UserControl
{
public pnlTools()
{
InitializeComponent();
}
private void Button_Click(object sender, RoutedEventArgs e)
{
TextBlock tb = new TextBlock();
tb.TextWrapping = TextWrapping.Wrap;
tb.Margin = new Thickness(10);
tb.Text = "A Text as Test";
cnvsObjects.Children.Add(tb); // Error!
}
}
}
As I've searched, I know in such cases I should use something like Dependency Properties. If it would have been a TextBlock, I could use Data Binding and a Dependency Property. But it is not a Property but a Method (Children.Add).
I'm new in WPF, so if All of things were in the MainWindow.Xaml, I had no problem. I have devided the MainWindos.xaml into some UserControls (nesting) to decrease the complexity and avoid a file to become huge. Did I choose UserControl for that purpose right? or should I use something else? What is the best way of doing that?
Sorry that this post became too long. I couldn't analyse other questions and answers related to this problem because they were so complex for me. Thanks.
CodePudding user response:
You are executing the Button.Click
event handlers in the wrong context. You would have to handle the Button.Click
event in a context that has access to the pnlCanvas
control, if this makes sense. Based on your posted example, the correct context would be the MainWindow
as it hosts both pnlCanvas
and pnlTools
.
The recommended solution would be to change your overall design. This would involve the introduction of data model classes, that represent/hold the data for a UIElement
, that you wish to add to the Canvas.
Such data would include the position on the canvas (x/y coordinates) and some data to display e.g., a text.
You would add those data models to a collection that serves as a binding source for a ListBox
. The Canvas
itself would be the panel of the ListBox
, by assigning it to the ListBox.ItemsPanel
property. You then define a DataTemplate
for each data model type, where the DataTemplate
contains the actual control you want to show. See Microsoft Docs: Data Templating Overview to learn about DataTemplate
.
However, to fix your example you must first let the PnlCanvas
control expose a public method that allows external controlslike the MainWindow
to add elements to its internal Canvas
(note that the proper naming for classes in C# would use PascalCasing.
For example pnlTools
should be PnlTools
. See Microsoft Docs: Naming Guidelines. All provided code examples will use the official C# naming convention):
public void AddText(string text, Point position)
{
var textBlock = new TextBlock() { Text = text };
// Position the element on the Canvas
Canvas.SetLeft(textBlock, position.X);
Canvas.SetTop(textBlock, position.Y);
this.cnvsObjects.Children.Add(textBlock);
}
Next, let PnlCanvas
expose a set of routed commands, that the buttons in the PnlTools
control can use. The MainWindow
will then listen to those commands. See Microsoft Docs: Commanding Overview
The complete PnlCanvas
class will then look as follows (example only shows adding text to the Canvas
or any other Panel
):
PnlCanvas.xaml.cs
public partial class PnlCanvas : UserControl
{
// TODO::Add commands to support other content
public static RoutedCommand AddTextCommand { get; } = new RoutedCommand("AddTextCommand", typeof(PnlCanvas));
public PnlCanvas()
{
InitializeComponent();
}
// TODO::Add methods to support other content
public void AddText(string text, Point position)
{
var textBlock = new TextBlock() { Text = text };
// Position the element on the Canvas
Canvas.SetLeft(textBlock, position.X);
Canvas.SetTop(textBlock, position.Y);
this.cnvsObjects.Children.Add(textBlock);
}
}
Then let PnlTools
use the routed commands defined in PnlCanvas
:
PnlTools.xaml
<UserControl>
<StackPanel>
<Button Content="Add Text"
Command="{x:Static local:PnlCanvas.AddTextCommand}" />
</StackPanel>
</UserControl>
Finally, let MainWindow
execute the command:
MainWindow.xaml.cs
partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
var addTextCommandBinding = new CommandBinding(PnlCanvas.AddTextCommand, ExecuteAddTextCommand, CanExecuteAddTextCommand);
this.CommandBindings.Add(addCircleCommandBinding);
}
private void CanExecuteAddTextCommand(object sender, CanExecuteRoutedEventArgs e)
{
e.CanExecute = true;
}
private void ExecuteAddTextCommand(object sender, ExecutedRoutedEventArgs e)
{
var textPosition = new Point(100, 100);
this.ElementHost.AddText("Some added text", textPosition);
}
}
CodePudding user response:
The general idea behind using UserControl
is two things:
- Keeping the code more readable, organized and easier to maintain and scale in the long run.
- Avoiding redundant code and repeated blocks of code. When the application uses similar UI patterns in different windows or pages, it is good practice to design a
UserControl
and use it anywhere it is needed.
Getting back to your main question, you can define a delegate/event in your PnlTools code-behind and register an event listener to it in the MainWindow code-behind.
PnlTools.xaml.cs:
public partial class PnlTools : UserControl
{
public delegate void OnClickEventHandler();
public event OnClickEventHandler OnClickEvent;
public PnlTools()
{
InitializeComponent();
}
private void Button_Click(object sender, RoutedEventArgs e)
{
OnClickEvent();
}
}
MainWindow.xaml:
<Window x:Class=...>
<Grid>
...
<local:PnlCanvas Grid.Column="2"/>
<GridSplitter Grid.Column="3" HorizontalAlignment="Stretch"/>
<local:PnlTools x:name="myPnlCanvas" Grid.Column="4" />
...
</Grid>
</Window>
MainWindow.xaml.cs:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
pnlTools.OnClickEvent = PnlTools_OnClickEvent;
}
private void PnlTools_OnClickEvent()
{
TextBlock tb = new TextBlock();
tb.TextWrapping = TextWrapping.Wrap;
tb.Margin = new Thickness(10);
tb.Text = "A Text as Test";
myPnlCanvas.cnvsObjects.Children.Add(tb);
}
}