Question

I have the following WPF window. Changing the value of #Cables adds (or removes) tabs to the TabControl (C2, C3, C4...). Changing #Phases adds new rows to the DataGrid.

Window

All of the tabs (other than this Options one) have the same format, so I created a UserControl NTab class which has its own .xaml and code-behind.

Now, each of the other tabs will have a ComboBox, where the user selects the appropriate Phase (by the name property). For that to be possible, the NTab needs to be aware of the Phase DatGrid in the Options tab. My code is currently split into two classes:

  • MainWindow, which contains the code (.xaml and code-behind) for the window as a whole and for the Options Tab;
  • NTab, which contains the code (.xaml and code-behind) for itself.

The Phase DataGrid's ItemSource is an ObservableCollection, so what I've done is sent the collection to the NTab constructor and tied its .CollectionChanged event to the following NTab function (Phases is an ObservableCollection<string> dependency property):

public void PhasesChanged(object source, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
    var pC = source as ObservableCollection<NPhase>;
    Phases.Clear();
    for (int i = 0; i < pC.Count; i++)
        Phases.Add("" + (i + 1));
}

This creates a copy of the DataGrid's data (just the names) and stores it inside each NTab as a dependency property bound to the ComboBox. This works.

My question is if there is a better way to do this. I would rather not have to create "redundant" copies of data and would like to be able to bind the ComboBoxes directly to the DataGrid. Is this possible?


EDIT: Adding my code. I've deleted parts of the .xaml and code-behind which weren't relevant to the question.

MainWindow.xaml

<Window x:Class="WPF.MainWindow"
    x:Name="Main"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:xctk="http://schemas.xceed.com/wpf/xaml/toolkit"
    xmlns:l="clr-namespace:WPF"
    Title="WPF" SizeToContent="WidthAndHeight">
<TabControl Name="tabControl" SelectedIndex="1">
    <TabItem Name="Options">
        <TabItem.Header>
            <TextBlock>Options</TextBlock>
        </TabItem.Header>
        <Grid>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto"></ColumnDefinition>
            </Grid.ColumnDefinitions>
            <Grid.RowDefinitions>
                <!-- ... -->
            </Grid.RowDefinitions>
            <Grid Grid.Row="0">
                <Grid.ColumnDefinitions>
                    <!-- ... -->
                </Grid.ColumnDefinitions>
                <Label Grid.Column="0"># Cables</Label>
                <xctk:IntegerUpDown Name="nCables" 
                                    Grid.Column="1" 
                                    Minimum="1" 
                                    Value="1"
                                    ValueChanged="nCablesChanged"/>
            </Grid>
            <Grid Grid.Row="1">
                <Grid.ColumnDefinitions>
                    <!-- ... -->
                </Grid.ColumnDefinitions>
                <Label Grid.Column="0"># Phases</Label>
                <xctk:IntegerUpDown Name="nPhases" 
                                    Grid.Column="1" 
                                    Minimum="1" 
                                    Value="4"
                                    ValueChanged="nPhasesChanged"/>
            </Grid>
            <l:NDataGrid Grid.Row="2"
                         x:Name="PhaseGrid"
                         ItemsSource="{Binding Phases, ElementName=Main}" 
                         LoadingRow="RowIndex"
                         CanUserAddRows="False"
                         CanUserDeleteRows="False"
                         CanUserReorderColumns="False"
                         CanUserSortColumns="False"
                         VerticalScrollBarVisibility="Hidden"
                         Block.TextAlignment="Center"/>
        </Grid>
    </TabItem>
</TabControl>
</Window>

MainWindow.xaml.cs

public partial class MainWindow : Window
{
    ObservableCollection<NPhase> Phases;
    public static DependencyProperty PhasesProperty = DependencyProperty.Register("Phases", typeof(ICollectionView), typeof(MainWindow));
    public ICollectionView IPhasesCollection
    {
        get { return (ICollectionView)GetValue(PhasesProperty); }
        set { SetValue(PhasesProperty, value); }
    }
    /// <summary>Controls the number of cables to be created</summary>
    /// <param name="sender">(IntegerUpDown)nCables</param>
    /// <param name="e">not used</param>
    private void nCablesChanged(object sender, RoutedEventArgs e)
    {
        int n = tabControl.Items.Count - 1;
        var o = sender as IntegerUpDown;
        int v = (int)o.Value;
        if (v > n)
        {
            for (int i = n; i < v; i++)
            {
                TabItem tab = new TabItem();
                tab.Header = "C" + (i + 1);
                tab.Content = new NTab(Phases);
                tabControl.Items.Add(tab);
            }
        }
        else if (v < n)
        {
            for (int i = v; i < n; i++)
                tabControl.Items.RemoveAt(n);
        }
    }
    /// <summary>Modifies the DataGrid according to the number of phases desired</summary>
    /// <param name="sender">(IntegerUpDown)nPhases</param>
    /// <param name="e">not used</param>
    private void nPhasesChanged(object sender, RoutedEventArgs e)
    {
        //...
    }
    /// <summary>Sets up the row headers</summary>
    /// <param name="sender">not used</param>
    /// <param name="e">Row to be modified</param>
    private void RowIndex(object sender, DataGridRowEventArgs e)
    {
        e.Row.Header = (e.Row.GetIndex() + 1).ToString();
    }
    public MainWindow()
    {
        Phases = new ObservableCollection<NPhase>();
        Phases.Add(new NPhase(3, "Protensao Inicial"));
        Phases.Add(new NPhase(28, "Carga Movel"));
        Phases.Add(new NPhase(365, "1 Ano"));
        Phases.Add(new NPhase(18250, "Vida Util"));
        IPhasesCollection = CollectionViewSource.GetDefaultView(Phases); 
        InitializeComponent();
    }
}

NTab.xaml

<UserControl x:Class="WPF.NTab"
         x:Name="CableTab"
         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
         xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
         xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
         xmlns:charting="clr-namespace:System.Windows.Forms.DataVisualization.Charting;assembly=System.Windows.Forms.DataVisualization"
         xmlns:xctk="http://schemas.xceed.com/wpf/xaml/toolkit"
         xmlns:l="clr-namespace:WPF"
         mc:Ignorable="d" >
<Grid>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="Auto"></ColumnDefinition>
    </Grid.ColumnDefinitions>
    <Grid.RowDefinitions>
        <!-- ... -->
    </Grid.RowDefinitions>
    <Grid Grid.Row="2">
        <Grid.ColumnDefinitions>
            <!-- ... -->
        </Grid.ColumnDefinitions>
        <Label Grid.Column="2">Pull Phase</Label>
        <ComboBox Grid.Column="3"
                  Name="Phase"
                  ItemsSource="{Binding Phases, ElementName=CableTab}"/>
    </Grid>
</Grid>
</UserControl>

NTab.xaml.cs

public partial class NTab : UserControl
{
    ObservableCollection<string> Phases;
    public static DependencyProperty PhasesProperty = DependencyProperty.Register("Phases", typeof(ICollectionView), typeof(NTab));
    public ICollectionView IPhasesCollection
    {
        get { return (ICollectionView)GetValue(PhasesProperty); }
        set { SetValue(PhasesProperty, value); }
    }
    /// <summary>Updates the tab's Phase list to match the list in the Options tab.</summary>
    /// <param name="source">(ObservableCollection&lt;NPhase&gt;</param>
    /// <param name="e">not used.</param>
    public void PhasesChanged(object source, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
    {
        var pC = source as ObservableCollection<NPhase>;
        Phases.Clear();
        for (int i = 0; i < pC.Count; i++)
            Phases.Add("" + (i + 1));
    }
    public NTab(ObservableCollection<NPhase> p)
    {
        InitializeComponent();
        Phases = new ObservableCollection<string>();
        IPhasesCollection = CollectionViewSource.GetDefaultView(Phases);
        PhasesChanged(p, new System.Collections.Specialized.NotifyCollectionChangedEventArgs(System.Collections.Specialized.NotifyCollectionChangedAction.Reset));
        p.CollectionChanged += PhasesChanged;
    }
}
Was it helpful?

Solution

You're looking at this in the wrong way... it is a rare situation in WPF when a UI control needs to know anything about another UI control. It is much more common to work with the data. By this, I mean that your NTab only needs to be aware of the data from the Phase DataGrid and not the control.

The simplest way to achieve this is to have one view model for the whole TabControl which shares its properties over the various TabItems. So rather than trying to handle UI events to keep the collections the same, just use one collection for all of the TabItems. In the DataGrid, you could do this:

<DataGrid ItemsSource="{Binding YourCollection}" ... />

On each additional TabItem, you could have this:

<ComboBox ItemsSource="{Binding YourCollection}" ... />

In this way, an update to the collection in one TabItem will automatically be reflected in all of the other TabItems, without you having to do anything.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top