Home > Enterprise >  Adding an UIElement to a Canvas (which is located in a UserControl) from another UserControl in wpf
Adding an UIElement to a Canvas (which is located in a UserControl) from another UserControl in wpf

Time:08-01

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:

  1. Keeping the code more readable, organized and easier to maintain and scale in the long run.
  2. 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);
    }
}
  • Related