I copied the code from the Xaml Controls Gallery. When I run and drag the listitem, the app closes. If I run in debug, F10 doesn't lead me to the part of the code that's breaking. In fact, the app stays open but nothing happens. I'm stumped.
(this is from winui3-preview 4)
Contact.txt
https://github.com/microsoft/Xaml-Controls-Gallery/blob/master/XamlControlsGallery/Assets/Contacts.txt
// ListBoxPage.xaml (I know bad naming)
<Page
x:Class="NavTest.ListBoxPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:NavTest"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local1="using:Windows.ApplicationModel.Contacts"
mc:Ignorable="d"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Page.Resources>
<!--
ListViews with grouped items must be bound to a CollectionViewSource, as shown below.
This CollectionViewSource is defined in the XAML below, but is bound to an ItemsSource in the C#
code-behind. See the C# code below for more details on how to create/bind to a grouped list.
-->
<CollectionViewSource x:Name="ContactsCVS" IsSourceGrouped="True"/>
<!--
In this example, the ListView's ItemTemplate property is bound to a data template (shown below)
called ContactListViewTemplate, defined in a Page.Resources section.
-->
<DataTemplate x:Key="ContactListViewTemplate" x:DataType="local:Contact">
<TextBlock Text="{x:Bind Name}" x:Phase="1" Margin="0,5,0,5"/>
</DataTemplate>
</Page.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<ListView
Grid.Row="0"
Grid.Column="0"
x:Name="BaseExample"
SelectionMode="Extended"
ItemsSource="{x:Bind ContactsCVS.View, Mode=OneWay}"
ItemTemplate="{StaticResource ContactListViewTemplate}"
BorderThickness="1"
BorderBrush="{ThemeResource SystemControlForegroundBaseMediumLowBrush}"
Width="550"
Height="400"
CanDragItems="True"
CanReorderItems="True"
AllowDrop="True"
DragItemsStarting="BaseExample_DragItemsStarting"
DragOver="BaseExample_DragOver"
Drop="BaseExample_Drop"
HorizontalAlignment="Left">
<ListView.ItemsPanel>
<ItemsPanelTemplate>
<ItemsStackPanel AreStickyGroupHeadersEnabled="True"/>
</ItemsPanelTemplate>
</ListView.ItemsPanel>
<ListView.GroupStyle>
<GroupStyle>
<GroupStyle.HeaderTemplate>
<DataTemplate x:DataType="local:GroupInfoList">
<Border>
<TextBlock Text="{x:Bind Key}" Style="{ThemeResource TitleTextBlockStyle}" />
</Border>
</DataTemplate>
</GroupStyle.HeaderTemplate>
</GroupStyle>
</ListView.GroupStyle>
</ListView>
<ListView
Grid.Row="0"
Grid.Column="1"
x:Name="BaseExample2"
SelectionMode="Extended"
ItemTemplate="{StaticResource ContactListViewTemplate}"
BorderThickness="1"
BorderBrush="{ThemeResource SystemControlForegroundBaseMediumLowBrush}"
Width="550"
Height="400"
CanDragItems="True"
CanReorderItems="True"
AllowDrop="True"
DragItemsStarting="BaseExample2_DragItemsStarting"
DragOver="BaseExample2_DragOver"
DragEnter="BaseExample2_DragEnter"
Drop="BaseExample_Drop"
HorizontalAlignment="Left">
</ListView>
</Grid>
// ListBoxPage.xaml.cs
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
using Microsoft.UI.Xaml.Data;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Navigation;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Runtime.InteropServices.WindowsRuntime;
using Windows.ApplicationModel.DataTransfer;
using Windows.Foundation;
using Windows.Foundation.Collections;
using Windows.Storage;
using Windows.UI.WebUI;
// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.
namespace NavTest
{
/// <summary>
/// An empty page that can be used on its own or navigated to within a Frame.
/// </summary>
public sealed partial class ListBoxPage : Page
{
ObservableCollection<Contact> contacts1 = new ObservableCollection<Contact>();
ObservableCollection<Contact> contacts2 = new ObservableCollection<Contact>();
public ListBoxPage()
{
this.InitializeComponent();
}
protected override async void OnNavigatedTo(NavigationEventArgs e)
{
// The ItemsSource for the ListView is generated by a method of the Contact class called
// GetContactsAsync().This method pulls data from an internal data source and creates
// Contact objects from that data. Those Contact objects are placed in a collection
// which is returned from the GetContactsAsync() function.
contacts1 = await Contact.GetContactsAsync();
contacts2 = new ObservableCollection<Contact>();
BaseExample.ItemsSource = contacts1;
BaseExample2.ItemsSource = contacts2;
ContactsCVS.Source = await Contact.GetContactsGroupedAsync();
}
private void BaseExample_DragItemsStarting(object sender, DragItemsStartingEventArgs e)
{
// Prepare a string with one dragged item per line
StringBuilder items = new StringBuilder();
foreach (Contact item in e.Items)
{
if (items.Length > 0) { items.AppendLine(); }
if (item.ToString() != null)
{
// Append name from contact object onto data string
items.Append(item.ToString() + " " + item.Company);
}
}
// Set the content of the DataPackage
e.Data.SetText(items.ToString());
e.Data.RequestedOperation = DataPackageOperation.Move;
}
private void BaseExample_DragOver(object sender, DragEventArgs e)
{
e.AcceptedOperation = DataPackageOperation.Move;
}
private async void BaseExample_Drop(object sender, DragEventArgs e)
{
ListView target = (ListView)sender;
if (e.DataView.Contains(StandardDataFormats.Text))
{
DragOperationDeferral def = e.GetDeferral();
string s = await e.DataView.GetTextAsync();
string[] items = s.Split('\n');
foreach (string item in items)
{
// Create Contact object from string, add to existing target ListView
string[] info = item.Split(" ", 3);
Contact temp = new Contact(info[0], info[1], info[2]);
// Find the insertion index:
Windows.Foundation.Point pos = e.GetPosition(target.ItemsPanelRoot);
// Find which ListView is the target, find height of first item
ListViewItem sampleItem;
if (target.Name == "BaseExample")
{
sampleItem = (ListViewItem)BaseExample2.ContainerFromIndex(0);
}
// Only other case is target = DragDropListView2
else
{
sampleItem = (ListViewItem)BaseExample.ContainerFromIndex(0);
}
// Adjust ItemHeight for margins
double itemHeight = sampleItem.ActualHeight + sampleItem.Margin.Top + sampleItem.Margin.Bottom;
// Find index based on dividing number of items by height of each item
int index = Math.Min(target.Items.Count - 1, (int)(pos.Y / itemHeight));
// Find the item that we want to drop
ListViewItem targetItem = (ListViewItem)target.ContainerFromIndex(index); ;
// Figure out if to insert above or below
Windows.Foundation.Point positionInItem = e.GetPosition(targetItem);
if (positionInItem.Y > itemHeight / 2)
{
index++;
}
// Don't go out of bounds
index = Math.Min(target.Items.Count, index);
// Find correct source list
if (target.Name == "BaseExample")
{
// Find the ItemsSource for the target ListView and insert
contacts1.Insert(index, temp);
//Go through source list and remove the items that are being moved
foreach (Contact contact in BaseExample2.Items)
{
if (contact.FirstName == temp.FirstName && contact.LastName == temp.LastName && contact.Company == temp.Company)
{
contacts2.Remove(contact);
break;
}
}
}
else if (target.Name == "BaseExample2")
{
contacts2.Insert(index, temp);
foreach (Contact contact in BaseExample.Items)
{
if (contact.FirstName == temp.FirstName && contact.LastName == temp.LastName && contact.Company == temp.Company)
{
contacts1.Remove(contact);
break;
}
}
}
}
e.AcceptedOperation = DataPackageOperation.Move;
def.Complete();
}
}
private void BaseExample_DragEnter(object sender, DragEventArgs e)
{
// We don't want to show the Move icon
e.DragUIOverride.IsGlyphVisible = false;
}
private void BaseExample2_DragItemsStarting(object sender, DragItemsStartingEventArgs e)
{
if (e.Items.Count == 1)
{
// Prepare ListViewItem to be moved
Contact tmp = (Contact)e.Items[0];
e.Data.SetText(tmp.FirstName + " " + tmp.LastName + " " + tmp.Company);
e.Data.RequestedOperation = DataPackageOperation.Move;
}
}
private void BaseExample2_DragOver(object sender, DragEventArgs e)
{
e.AcceptedOperation = DataPackageOperation.Move;
}
private async void BaseExample2_Drop(object sender, DragEventArgs e)
{
ListView target = (ListView)sender;
if (e.DataView.Contains(StandardDataFormats.Text))
{
DragOperationDeferral def = e.GetDeferral();
string s = await e.DataView.GetTextAsync();
string[] items = s.Split('\n');
foreach (string item in items)
{
// Create Contact object from string, add to existing target ListView
string[] info = item.Split(" ", 3);
Contact temp = new Contact(info[0], info[1], info[2]);
// Find the insertion index:
Windows.Foundation.Point pos = e.GetPosition(target.ItemsPanelRoot);
// Find which ListView is the target, find height of first item
ListViewItem sampleItem;
if (target.Name == "BaseExample")
{
sampleItem = (ListViewItem)BaseExample2.ContainerFromIndex(0);
}
// Only other case is target = DragDropListView2
else
{
sampleItem = (ListViewItem)BaseExample.ContainerFromIndex(0);
}
// Adjust ItemHeight for margins
double itemHeight = sampleItem.ActualHeight + sampleItem.Margin.Top + sampleItem.Margin.Bottom;
// Find index based on dividing number of items by height of each item
int index = Math.Min(target.Items.Count - 1, (int)(pos.Y / itemHeight));
// Find the item that we want to drop
ListViewItem targetItem = (ListViewItem)target.ContainerFromIndex(index); ;
// Figure out if to insert above or below
Windows.Foundation.Point positionInItem = e.GetPosition(targetItem);
if (positionInItem.Y > itemHeight / 2)
{
index++;
}
// Don't go out of bounds
index = Math.Min(target.Items.Count, index);
// Find correct source list
if (target.Name == "BaseExample")
{
// Find the ItemsSource for the target ListView and insert
contacts1.Insert(index, temp);
//Go through source list and remove the items that are being moved
foreach (Contact contact in BaseExample2.Items)
{
if (contact.FirstName == temp.FirstName && contact.LastName == temp.LastName && contact.Company == temp.Company)
{
contacts2.Remove(contact);
break;
}
}
}
else if (target.Name == "BaseExample2")
{
contacts2.Insert(index, temp);
foreach (Contact contact in BaseExample.Items)
{
if (contact.FirstName == temp.FirstName && contact.LastName == temp.LastName && contact.Company == temp.Company)
{
contacts1.Remove(contact);
break;
}
}
}
}
e.AcceptedOperation = DataPackageOperation.Move;
def.Complete();
}
}
private void BaseExample2_DragEnter(object sender, DragEventArgs e)
{
// We don't want to show the Move icon
e.DragUIOverride.IsGlyphVisible = false;
}
}
// C# code-behind
// The data template is defined to display a Contact object (class definition shown below), and the text
// displayed is bound to the Contact object's Name attribute.
public class Contact
{
public string FirstName { get; private set; }
public string LastName { get; private set; }
public string Company { get; private set; }
public string Name => FirstName + " " + LastName;
public static string ContactsPath => $#"{AppDomain.CurrentDomain.BaseDirectory}\Assets\Contacts.txt";
public Contact(string firstName, string lastName, string company)
{
FirstName = firstName;
LastName = lastName;
Company = company;
}
public override string ToString()
{
return Name;
}
#region Public Methods
public static async Task<ObservableCollection<Contact>> GetContactsAsync()
{
string contactsPath = $#"{AppDomain.CurrentDomain.BaseDirectory}\Assets\Contacts.txt";
Uri contactsUri = new Uri(contactsPath, UriKind.RelativeOrAbsolute);
Uri _contactsUri = new Uri(ContactsPath, UriKind.RelativeOrAbsolute);
StorageFile file = await StorageFile.GetFileFromPathAsync(contactsPath);
IList<string> lines = await FileIO.ReadLinesAsync(file);
ObservableCollection<Contact> contacts = new ObservableCollection<Contact>();
for (int i = 0; i < lines.Count; i += 3)
{
contacts.Add(new Contact(lines[i], lines[i + 1], lines[i + 2]));
}
return contacts;
}
// To create a collection of grouped items, create a query that groups
// an existing list, or returns a grouped collection from a database.
// The following method is used to create the ItemsSource for our CollectionViewSource:
public static async Task<ObservableCollection<GroupInfoList>> GetContactsGroupedAsync()
{
// Grab Contact objects from pre-existing list (list is returned from function GetContactsAsync())
var query = from item in await GetContactsAsync()
// Group the items returned from the query, sort and select the ones you want to keep
group item by item.LastName.Substring(0, 1).ToUpper() into g
orderby g.Key
// GroupInfoList is a simple custom class that has an IEnumerable type attribute, and
// a key attribute. The IGrouping-typed variable g now holds the Contact objects,
// and these objects will be used to create a new GroupInfoList object.
select new GroupInfoList(g) { Key = g.Key };
return new ObservableCollection<GroupInfoList>(query);
}
#endregion
}
// GroupInfoList class definition:
public class GroupInfoList : List<object>
{
public GroupInfoList(IEnumerable<object> items) : base(items)
{
}
public object Key { get; set; }
}
}
Drag and Drop is not on the list of supported functionality in and preview 0.5
I think they fixed a few serious bugs in 0.5.
Try again when 0.8 is released.
Remember you are one of the earlierst birds when you use WinUI3 now. You don't get only the worm but also the mud.
I think the problem is not in the posted code, I think you changed App.xaml and App.xaml.cs or (not xor) removed the default reference MainWindow in order to make your app. If your app does not start and does not reach any break point in your Page, it's because it is not in the "execution chain" which starts with App.xaml.
App.xaml calls the MainWindow class (View):
<Application x:Class="MyProject.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:MyProject"
StartupUri="MainWindow.xaml"> <!-- reference here-->
<Application.Resources>
</Application.Resources>
</Application>
And then, from MainWindow (.xaml and .xaml.cs) you should have your Page referenced.
Related
Problem
A user WPF control is made up of multiple standard controls.
How can multiple dependency properties of the component (base or standard) controls be accessed in XAML, when implementing the parent (user) control, without creating additional properties?
Details
What do I mean by "creating additional dependency properties"? Well, that is the only way I know of accessing properties of the component controls: by implementing attached properties, as described at MSDN here.
However, it presents the following problems:
Existing dependency properties must be copied as new properties, defeating the DRY principle.
If data binding is to occur, more work must be done to bind existing dependency properties to the new exposed dependency properties.
I'm wondering if there is a way to "walk" the base controls within the user control, to access their properties - from within XAML.
Example
For example, I make a user WPF control that inherits from UserControl. It is simple - it consists of a StackPanel containing a Label and a TextBlock:
<UserControl x:Class="MyApp.CustomControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<StackPanel>
<Label Name="BaseLabel">Label Here</Label>
<TextBlock Name="BaseTextBlock">Some text here.</TextBlock>
</StackPanel>
</UserControl>
Now, when I use my UserControl elsewhere in XAML, I'm wishfully thinking something like this could be done to edit my Label's content... although I don't know of a way:
<Window x:Class="MyApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:MyApp">
<StackPanel>
<!-- This won't work, don't try at home kids. -->
<local:CustomControl BaseLabel.Content="I did it!"></local:CustomControl>
</StackPanel>
</Window>
Much thanks.
How about the next solution:
1. Create the AttachedProperty (because you must an entry point) and bind this property to the collection of data.This collection of data will contain changes you want perform on sub-controls of a main user control used inside the window. This collection will be defined inside the main window view model.
2. In attached property changed callback get the binded collection, parse it data into sub-controls properties.
Here is the solution:
3. Xaml code:
<Window.DataContext>
<nirHelpingOvalButton:MainWindowViewModel />
</Window.DataContext>
<Grid>
<nirHelpingOvalButton:InnerControl x:Name="MyInnerControl"
nirHelpingOvalButton:Helper.InnerControlPropertiesAccessor="{Binding InnerData, Mode=Default, UpdateSourceTrigger=PropertyChanged}"/>
</Grid>
4. Attached property code (bindig support):
public static readonly DependencyProperty InnerControlPropertiesAccessorProperty = DependencyProperty.RegisterAttached(
"InnerControlPropertiesAccessor", typeof (ObservableCollection<TargetControlData>), typeof (Helper), new PropertyMetadata(default(ObservableCollection<TargetControlData>), InnerValueAccessProviderPropertyChangedCallback));
public static void SetInnerControlPropertiesAccessor(DependencyObject element, ObservableCollection<TargetControlData> value)
{
element.SetValue(InnerControlPropertiesAccessorProperty, value);
}
public static ObservableCollection<TargetControlData> GetInnerControlPropertiesAccessor(DependencyObject element)
{
return (ObservableCollection<TargetControlData>) element.GetValue(InnerControlPropertiesAccessorProperty);
}
private static void InnerValueAccessProviderPropertyChangedCallback(DependencyObject sender, DependencyPropertyChangedEventArgs args)
{
var control = sender as Control;
if (control == null) return;
var valuesMap = args.NewValue as ObservableCollection<TargetControlData>;
if (valuesMap == null)
return;
valuesMap.ToList().ForEach(data => TryToBind(control, data));
}
private static void TryToBind(Control control, TargetControlData data)
{
var innerControl = control.FindName(data.SubControlName) as DependencyObject;
if (innerControl == null) return;
var myBinding = new Binding
{
Source = data,
Path = new PropertyPath("Data"),
Mode = BindingMode.TwoWay,
UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged
};
var descriptors = TypeDescriptor.GetProperties(innerControl);
var propertyDescriptor = descriptors.Find(data.SubConrolProperty, true);
var descriptor = DependencyPropertyDescriptor.FromProperty(propertyDescriptor);
if (descriptor == null) return;
var dependencyProperty = descriptor.DependencyProperty;
BindingOperations.SetBinding(innerControl, dependencyProperty, myBinding);
}
5. Inner control xaml:
<UserControl x:Class="NirHelpingOvalButton.InnerControl"
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"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<UniformGrid>
<Button x:Name="InnerControlButton"></Button>
<TextBlock x:Name="InnerContentTextBlock"></TextBlock>
</UniformGrid>
6. ViewModel code:
public class MainWindowViewModel:BaseObservableObject
{
private static int _staticCount = 0;
private List<Brush> _list = new List<Brush> {Brushes.Green, Brushes.Red, Brushes.Blue};
public MainWindowViewModel()
{
InnerData = new ObservableCollection<TargetControlData>
{
new TargetControlData
{
SubControlName = "InnerControlButton",
SubConrolProperty = "Content",
Data = "Click Me",
},
new TargetControlData
{
SubControlName = "InnerControlButton",
SubConrolProperty = "Command",
Data = new RelayCommand(CommandMethod),
},
new TargetControlData
{
SubConrolProperty = "Text",
SubControlName = "InnerContentTextBlock",
Data = "Hello"
},
new TargetControlData
{
SubConrolProperty = "Background",
SubControlName = "InnerContentTextBlock",
Data = Brushes.Green
},
new TargetControlData
{
SubConrolProperty = "Foreground",
SubControlName = "InnerContentTextBlock",
Data = Brushes.White
},
};
}
private void CommandMethod()
{
_staticCount ++;
var backgroundData = InnerData.FirstOrDefault(data => data.SubControlName == "InnerContentTextBlock" && data.SubConrolProperty == "Background");
var textData = InnerData.FirstOrDefault(data => data.SubControlName == "InnerContentTextBlock" && data.SubConrolProperty == "Text");
if (backgroundData == null || textData == null) return;
var index = _staticCount%_list.Count;
backgroundData.Data = _list[index];
textData.Data = string.Format("{0} {1}", "Hello", backgroundData.Data);
}
public ObservableCollection<TargetControlData> InnerData { get; set; }}
7. TargetControlData code:
public class TargetControlData:BaseObservableObject
{
private string _subControlName;
private string _subConrolProperty;
private object _data;
public string SubControlName
{
get { return _subControlName; }
set
{
_subControlName = value;
OnPropertyChanged();
}
}
public string SubConrolProperty
{
get { return _subConrolProperty; }
set
{
_subConrolProperty = value;
OnPropertyChanged();
}
}
public object Data
{
get { return _data; }
set
{
_data = value;
OnPropertyChanged();
}
}
}
Summary - you can pull control properties data from configuration file, or collect them by reflection.
regards,
The way you suggested - I don't think this would be possible.
But it can be done with normal properties, instead of dependency properties, something like:
UserControl xaml:
<StackPanel>
<TextBlock x:Name="tbOne"></TextBlock>
<TextBlock x:Name="tbTwo" Foreground="Red"></TextBlock>
</StackPanel>
UserControl code behind:
public string One
{
get
{
return this.tbOne.Text;
}
set
{
this.tbOne.Text = value;
}
}
public string Two
{
get
{
return this.tbTwo.Text;
}
set
{
this.tbTwo.Text = value;
}
}
and the usage of user control:
<local:UserControl1 One="test1" Two="test2"></local:UserControl1>
How to clean the previously selected row ?
I have this method that search for item with a given id.
private void TreeView_OnSelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
{
foreach (MessageFieldViewModel rowItem in Datagrid.ItemsSource)
{
if (_mainWindowModel != null)
_mainWindowModel.SelectedMessageElement = (MessageElementViewModel)e.NewValue;
var row = Datagrid.ItemContainerGenerator.ContainerFromItem(rowItem) as DataGridRow;
if (_mainWindowModel != null && _mainWindowModel.SelectedMessageElement != null)
{
if (rowItem.Id == _mainWindowModel.SelectedMessageElement.Id)
{
if (row != null)
row.Background = Brushes.DarkSalmon;
}
row.Background.ClearValue();
}
// if (item != null) row.Background.ClearValue(rowItem.Id);
// break;
}
This selects the row of a given id. but if I want to select another Id The previous id is still selected.
how can I remove previously selected Ids and show only the newly selected id?
ok so here we go
XAML
<TreeView Name="Tree" Width="50" Height="100"
ItemsSource="{Binding YourTree}"
SelectedItemChanged="Tree_SelectedItemChanged"
SelectedValuePath="yourIdProperty"/>
<DataGrid Name="myDatagrid" Width="100" Height="100"
ItemsSource="{Binding YourList}"
SelectedItem="{Binding YourSelectedItem,UpdateSourceTrigger=PropertyChanged}"
SelectedValuePath="Id"/>
Codebehind
private void Tree_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
{
var tree = sender as TreeView;
myDatagrid.SelectedValue = tree.SelectedValue;
}
ViewModel
public List<yourClass> YourList { get; set; }
private yourClass yourSelectedItem;
public yourClass YourSelectedItem
{
get { return yourSelectedItem; }
set
{
yourSelectedItem = value;
OnPropertyChanged("YourSelectedItem");
}
}
you need to set the SelectedValuePath based your Property which represent your Id but not as Binding just as string as example "Id"
for my project, i am trying to create a signal channel generator which connects to a toolset and pushes signals into it.
the issue i have is that i have been given the project in a form where the code for the textboxes are in the codebehind file, and i would like them to be in the xaml.
i have a variable which controls the number of channels (viewmodels) which can be changed. which is able to create multiple instances of the same viewmodel on the window. this allows the ability to select different targets inside the tool whcih it is communicating with and be able to pump signals to each target.
here is the code currently in the XAML:
<Window x:Class="SigGeneratorMVVM.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:SigGeneratorMVVM"
Title="Signal Generator" Height="370" Width="734" >
<StackPanel Name="MyWindow">
<!--<TextBox Height="23" HorizontalAlignment="Left" Margin="91,20,0,0" Name="CurrentValDisplay" VerticalAlignment="Top" Width="120" />-->
</StackPanel>
</Window>
here is the code for the mainwindow.cs
public partial class MainWindow : Window
{
List<ViewModel> gViewModels;
int gNumChannels = 1;
private System.Threading.Timer mViewUpdateTimer;
private TimerCallback mViewTimerCallback;
private UtilityParticipant mParticipant;
public MainWindow()
{
InitializeComponent();
// Connect as UtilityParticipant
ConnectMesh();
gViewModels = new List<ViewModel>();
for (int i = 0; i < gNumChannels; i++)
{
gViewModels.Add(new ViewModel(mParticipant));
TextBlock CurrentValueText = new TextBlock();
CurrentValueText.Text = "Current Value:";
CurrentValueText.Margin = new Thickness(5);
TextBox CurrentValueBox = new TextBox();
CurrentValueBox.Width = 120;
CurrentValueBox.Name = "CurrentValDisplay" + i.ToString();
CurrentValueBox.HorizontalAlignment = System.Windows.HorizontalAlignment.Left;
CurrentValueBox.Margin = new Thickness(10);
CurrentValueBox.SetBinding(TextBox.TextProperty, "CurrentValue");
//CurrentValDisplay.Name = "CurrentValDisplay" + i.ToString();
//CurrentValDisplay.SetBinding(TextBox.TextProperty, "CurrentValue");
TextBlock CurrentFrequencyText = new TextBlock();
CurrentFrequencyText.Text = "Frequency:";
CurrentFrequencyText.Margin = new Thickness(5);
TextBox CurrentFrequencyBox = new TextBox();
CurrentFrequencyBox.Width = 120;
CurrentFrequencyBox.Name = "CurrentFrequencyDisplay" + i.ToString();
CurrentFrequencyBox.HorizontalAlignment = System.Windows.HorizontalAlignment.Left;
CurrentFrequencyBox.Margin = new Thickness(10);
CurrentFrequencyBox.SetBinding(TextBox.TextProperty, "Frequency");
Slider FrequencySlider = new Slider();
FrequencySlider.Width = 200;
FrequencySlider.Name = "FrequencySet" + i.ToString();
FrequencySlider.Value= 10;
FrequencySlider.Maximum = 10;
FrequencySlider.Minimum = 0.1;
FrequencySlider.SetBinding(Slider.ValueProperty, "Frequency");
//Create a new stackpanel
StackPanel sp = new StackPanel();
sp.Orientation = Orientation.Vertical;
//Set DataContext of the StackPanel
sp.DataContext = gViewModels[i];
//Add controls created above to the StackPanel
sp.Children.Add(CurrentValueText);
sp.Children.Add(CurrentValueBox);
sp.Children.Add(CurrentFrequencyText);
sp.Children.Add(CurrentFrequencyBox);
sp.Children.Add(FrequencySlider);
//Add the StackPanel to the window
MyWindow.Children.Add(sp);
}
mViewTimerCallback = this.UpdateView;
mViewUpdateTimer = new System.Threading.Timer(mViewTimerCallback, null, 100, 20);
}
Update: I already have a ViewModel which has get set methods for each property (CurrentValue and Frequency for now), would it be sufficient to bind the DataTemplate and ItemsControl to that instead of creating a new model class?
private SigGenChannel mSigGenChannel;
//Constructor
public ViewModel(UtilityParticipant aParticipant)
{
mSigGenChannel = new SigGenChannel(aParticipant);
}
public string CurrentValue
{
get
{
return mSigGenChannel.CurrentValue.ToString();
}
set
{
mSigGenChannel.CurrentValue = double.Parse(value);
RaisePropertyChanged("CurrentValue");
}
}
public double Frequency
{
get
{
return mSigGenChannel.Frequency;
}
set
{
mSigGenChannel.Frequency = value;
RaisePropertyChanged("Frequency");
}
}
public double Amplitude
{
get
{
return mSigGenChannel.Amplitude;
}
set
{
mSigGenChannel.Amplitude = value;
RaisePropertyChanged("Amplitude");
}
}
public void RefreshValue()
{
//A bit of a cheat, but we provide a means to poke the Viewmodel
//And raise a property change event
RaisePropertyChanged("CurrentValue");
}
also this is the SigChannel model:
class SigGenChannel
{
#region Private members
private UtilityParticipant mParticipant;
private double mCurrentValue;
private double mFrequency;
private double mAmplitude;
private double mTarget;
private double mOffset;
private double mCurrentStepTime;
private DateTime mStartTime;
private System.Threading.Timer mTimer;
private TimerCallback mTCallback;
private int mUpdateInterval = 10;
#endregion
#region Public members
public double CurrentValue
{
get
{
return mCurrentValue;
}
set
{
mCurrentValue = value;
}
}
public double Frequency
{
get
{
return mFrequency;
}
set
{
mFrequency = value;
}
}
public double Amplitude
{
get
{
return mAmplitude;
}
set
{
mAmplitude = value;
}
}
public double Target
{
get
{
return mTarget;
}
set
{
mTarget = value;
}
}
#endregion
//Constructor
public SigGenChannel(UtilityParticipant aParticipant)
{
mParticipant = aParticipant;
mCurrentValue = 10;
mFrequency = 200;
mAmplitude = 100;
mOffset = 0;
mCurrentStepTime = 0;
mStartTime = DateTime.Now;
mTCallback = this.Update;
mTimer = new System.Threading.Timer(mTCallback, null, 500, mUpdateInterval);
//Array enumData = Enum.GetNames;
//RefreshItems();
//Temp Code....!
Collection lCollection = mParticipant.GetCollection("DefaultNodeName.NodeStats");
lCollection.Publish();
}
private void Update(object StateInfo)
{
TimeSpan span = DateTime.Now - mStartTime;
mCurrentStepTime = span.TotalMilliseconds / (double)1000;
mCurrentValue = (Math.Sin(mCurrentStepTime * (mFrequency * 2 * Math.PI)) * mAmplitude / 2) + mOffset;
//Temp Code...!
Collection lCollection = mParticipant.GetCollection("DefaultNodeName.NodeStats");
Parameter lParameter = lCollection.GetParameter("CPUPercent");
lParameter.SetValue(mCurrentValue);
lCollection.Send();
The current code is written in a way that does not follow the WPF suggested practices, and swimming against the current makes things a lot harder than then should be.
What the code should be doing is:
Create a (view)model class for a channel
For example:
class ChannelModel
{
public int Value { get; set; }
public int Frequency { get; set; }
}
Use an ItemsControl instead of a StackPanel
The WPF way of doing things like this is to bind controls to collections, so replace the StackPanel with an ItemsControl.
Bind the ItemsControl to an ObservableCollection of the models
Your main viewmodel should expose an ObservableCollection<ChannelModel> property and the control should bind to that directly:
<ItemsControl ItemsSource="{Binding CollectionOfChannelModels}"/>
This ensures that the control is automatically updated with any changes made to your collection without your needing to do anything else.
Use a DataTemplate to specify how each model should render
So far we 've gotten the control to stay in sync with your channel collection, but we also need to tell it how each item (channel model) should be displayed. To do this, add a DataTemplate to the Resources collection of the ItemsControl:
<ItemsControl.Resources>
<DataTemplate DataType={x:Type local:ChannelModel}>
<StackPanel Orientation="Horizontal">
<TextBlock Text="Value" />
<TextBox Text="{Binding Value}" />
</StackPanel>
</DataTemplate>
</ItemsControl.Resources>
Generally the idea is to create a DataTemplate for a specific type, in your case it is for ViewModel.
Create a DataTemplate for your ViewModel, for instance:
<DataTemplate DataType={x:Type local:ViewModel}>
<TextBox Text="{Binding ViewModelTextProperty}" />
</DataTemplate>
And also in your XAML you must bind your list of ViewModels
<ItemsControl ItemsSource="{Binding myListOfViewModels}"/>
I'm trying to implement column chooser functionality for a DataGrid and am running into problems if I try to define the content of the header for the column as something more than just a string. Below is a very simplified example with all styles, view models, binding, etc all stripped out.
There are 3 columns:
The first column uses a string for the header.
The second column tries to set the header content to a Label with a ToolTip.
The third column ties to set the header content to a TextBlock with a ToolTip.
Clicking the Toggle Visibility button for Column A works fine. The Toggle Visibility buttons for both Columns B and C cause an InvalidOperationException with the message "Specified element is already the logical child of another element. Disconnect it first."
<Window x:Class="DataGridColumnChoosing.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal" Margin="0,10">
<TextBlock Margin="15, 0">Toggle Visibility:</TextBlock>
<Button Click="ToggleA">Column A</Button>
<Button Click="ToggleB">Column B</Button>
<Button Click="ToggleC">Column C</Button>
</StackPanel>
<!-- Main Fuel Mileage Datagrid -->
<DataGrid x:Name="mySampleDataGrid" Grid.Row="1"
AutoGenerateColumns="False" CanUserSortColumns="False" CanUserResizeRows="False" CanUserAddRows="False"
GridLinesVisibility="All" RowHeaderWidth="0">
<DataGrid.Columns>
<DataGridTemplateColumn x:Name="colA" Width="40*" IsReadOnly="True" Header="Column A">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn x:Name="colB" Width="40*" IsReadOnly="True" >
<DataGridTemplateColumn.Header>
<Label Content="Column B" ToolTip="A short explanation of Column B"/>
</DataGridTemplateColumn.Header>
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn x:Name="colC" Width="40*" IsReadOnly="True" >
<DataGridTemplateColumn.Header>
<TextBlock Text="Column C" ToolTip="A short explanation of Column C " />
</DataGridTemplateColumn.Header>
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
</Grid>
The simple click event handlers for the buttons that are toggling the visibility in this example are simply modifying the visibility of the columns.
private void ToggleA(object sender, RoutedEventArgs e)
{
colA.Visibility = colA.Visibility == System.Windows.Visibility.Visible ? System.Windows.Visibility.Hidden : System.Windows.Visibility.Visible;
}
private void ToggleB(object sender, RoutedEventArgs e)
{
colB.Visibility = colB.Visibility == System.Windows.Visibility.Visible ? System.Windows.Visibility.Hidden : System.Windows.Visibility.Visible;
}
private void ToggleC(object sender, RoutedEventArgs e)
{
colC.Visibility = colC.Visibility == System.Windows.Visibility.Visible ? System.Windows.Visibility.Hidden : System.Windows.Visibility.Visible;
}
Thanks all.
I had this issue once when I had a control defined in my Resources, and was trying to use it within multiple control's Content areas. That does not work because the control can only belong to one parent.
Instead, I needed to define a Template of some kind which contained the control I wanted, and set the Template of my object instead of the content directly.
Your comment on #Gimno's answer makes me think this is the case.
Try changing it so instead of setting a Label/TextBox in DataGrid.Header's content directly, set DataGrid.HeaderTemplate to a DataTemplate which contains the Label or TextBox.
EDIT
Here's some example code
<DataGridTemplateColumn x:Name="colB" Width="40*" IsReadOnly="True" >
<DataGridTemplateColumn.HeaderTemplate>
<DataTemplate>
<Label Content="Column B" ToolTip="A short explanation of Column B"/>
</DataTemplate>
</DataGridTemplateColumn.HeaderTemplate>
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
I think it would be easiest if you just use DataGridTemplateColumn.HeaderStyle instead of DataGridTemplateColumn.Header
As example for column c:
<DataGridTemplateColumn x:Name="colC" Width="40*" IsReadOnly="True" >
<DataGridTemplateColumn.HeaderStyle>
<Style TargetType="DataGridColumnHeader">
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate>
<TextBlock Text="Column C" ToolTip="A short explanation of Column C "/>
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
</DataGridTemplateColumn.HeaderStyle>
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGridTemplateColumn.HeaderStyle>
I liked the column chooser solution from CodePlex:
DataGrid Behavior
I clean up the code and remove unnecessary code:
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Media;
using Microsoft.Xaml.Behaviors;
namespace Behaviors
{
public class WpfDataGridConfigurationBehavior : Behavior<DependencyObject>
{
#region "public Properties"
public bool DontPersistVisibleColumns { get; set; }
public bool DontPersistColumnsOrder { get; set; }
public string ContextMenuChoices { get; set; } = "Alphabetical Menu,Show All Columns";
public string DataGridName { get; set; }
#endregion "public Properties"
#region "private Properties"
private DataGrid dataGrid;
private ContextMenu theContextMenu; // Context Menu for the field chooser.
private string AllColumnsHeaders { get; set; }
private string AllColumnDisplayIndexes { get; set; }
private int nBaseItems = 5;
private MenuItem mnuAlpha;
private MenuItem mnuShowAll;
#endregion "Private Properties"
protected override void OnAttached()
{
base.OnAttached();
dataGrid = this.AssociatedObject as DataGrid;
if (DataGridName == null) DataGridName = dataGrid.Name;
ContextMenu_BuildStaticMenu();
dataGrid.Loaded += (sender, e) => { DataGrid_Loaded(); };
dataGrid.AutoGeneratedColumns += DataGrid_AutoGeneratedColumns;
dataGrid.ColumnReordered += (sender, e) => { DataGrid_ColumnReordered(sender); };
dataGrid.ContextMenuOpening += (sender, e) => { DataGrid_ContextMenuOpening(e); };
dataGrid.LoadingRow += (sender, e) => { e.Row.Header = (e.Row.GetIndex() + 1).ToString(); };
dataGrid.RowHeaderWidth = 0;
var ns = new Style(typeof(DataGridRowHeader)) { BasedOn = dataGrid.RowHeaderStyle };
ns.Setters.Add(new Setter(Control.FontWeightProperty, FontWeights.Bold));
ns.Setters.Add(new Setter(Control.FontSizeProperty, 11.0));
ns.Setters.Add(new Setter(Control.PaddingProperty, new Thickness(4, 0, 4, 0)));
dataGrid.RowHeaderStyle = ns;
}
private void DataGrid_AutoGeneratedColumns(object sender, EventArgs e)
{
DataGrid_Loaded();
}
#region "DataGrid Events"
private void DataGrid_ContextMenuOpening(ContextMenuEventArgs e)
{
var dep = (DependencyObject)e.OriginalSource;
// iteratively traverse the visual tree
while ((dep != null) && !(dep is DataGridCell) && !(dep is DataGridColumnHeader))
dep = VisualTreeHelper.GetParent(dep);
if (dep == null)
return;
if (dep is DataGridColumnHeader)
{
// do something
}
if (dep is DataGridCell)
{
// navigate further up the tree
while ((dep != null) && !(dep is DataGridRow))
dep = VisualTreeHelper.GetParent(dep);
var row = dep as DataGridRow;
dataGrid.ItemContainerGenerator.IndexFromContainer(row);
}
}
private void DataGrid_ColumnReordered(object sender)
{
Settings_SaveDisplayIndexes(sender);
ContextMenu_BuildMenu();
}
private void DataGrid_Loaded()
{
ContextMenu_BuildMenu(false);
VisibleColumns_Initialize();
}
#endregion "DataGrid Events"
#region "ContextMenu Methods and Events"
private void ContextMenu_BuildStaticMenu()
{
theContextMenu = new ContextMenu { FontSize = 11, StaysOpen = true };
mnuAlpha = new MenuItem
{
Header = ContextMenuChoices.Split(',')[0],
FontWeight = FontWeights.Bold,
IsCheckable = true,
StaysOpenOnClick = true
};
mnuAlpha.Click += (sender, e) => { ContextMenu_BuildMenu(); };
mnuShowAll = new MenuItem
{
Header = ContextMenuChoices.Split(',')[1],
FontWeight = FontWeights.Bold,
IsCheckable = true,
StaysOpenOnClick = true
};
mnuShowAll.Checked += (sender, e) => { VisibleColumns = AllColumnsHeaders; };
mnuShowAll.Click += (sender, e) =>
{
if (mnuShowAll.IsChecked == false) VisibleColumns_Initialize();
};
theContextMenu.Items.Add(mnuShowAll);
theContextMenu.Items.Add(mnuAlpha);
}
private void ContextMenu_BuildMenu(bool pbRebuild = true)
{
for (int i = theContextMenu.Items.Count - 1; i > 0; i--)
if (((MenuItem)theContextMenu.Items[i]).FontWeight != FontWeights.Bold)
theContextMenu.Items.Remove(theContextMenu.Items[i]);
nBaseItems = theContextMenu.Items.Count;
// Attach the context menu to the DataGrid ColumnHeaders
var headersPresenter = WpfDataGridConfigurationBehaviorFinder.FindChild<DataGridColumnHeadersPresenter>(dataGrid);
ContextMenuService.SetContextMenu(headersPresenter, theContextMenu);
if (VisibleColumns == null)
throw (new SettingsPropertyNotFoundException("User's VisibleColumns setting not found."));
// Get the current column ordering from user.config
if (DisplayIndexes == null)
throw (new SettingsPropertyNotFoundException("User's DisplayIndexes setting not found."));
AllColumnDisplayIndexes = DisplayIndexes;
string[] colIndexes = AllColumnDisplayIndexes.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
var dataGridColumns = dataGrid.Columns.OrderBy(x => x.DisplayIndex);
// Sort the columns in display index order so menu header order matchs display column order
if (pbRebuild == false)
// Initially the datagrid column order is such that display indexes are the same as the col indexes
if (colIndexes.Length > 0)
dataGridColumns = dataGrid.Columns.OrderBy(x => Convert.ToInt16(colIndexes[x.DisplayIndex]));
if (mnuAlpha.IsChecked)
dataGridColumns = dataGrid.Columns.OrderBy(x => x.Header.ToString());
AllColumnsHeaders = "";
foreach (var col in dataGridColumns)
{
// All column name to a list of all column headers for later use.
AllColumnsHeaders = $"{col.Header.ToString().Replace("\n", " ").Replace("\r", " ")};{AllColumnsHeaders}";
// Add new menu item in display order.
ContextMenu_AddNewMenuItem(col);
}
string sTemp = VisibleColumns;
VisibleColumns = null;
VisibleColumns = sTemp;
}
private void ContextMenu_AddNewMenuItem(DataGridColumn col)
{
var menuItem = new MenuItem { Header = col.Header.ToString().Replace("\n", " ").Replace("\r", " "), StaysOpenOnClick = true };
var saVisibleColumns = new List<string> { string.Empty };
if (VisibleColumns != null)
{
saVisibleColumns = VisibleColumns.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries).ToList();
}
menuItem.IsChecked = (saVisibleColumns.Contains(menuItem.Header));
menuItem.Click += (sender, e) => { ContextMenu_ColumnName_Click(sender); };
theContextMenu.Items.Add(menuItem);
}
private void ContextMenu_ColumnName_Click(object sender)
{
var mi = sender as MenuItem;
// Get the column name that was clicked
string colName = mi.Header.ToString();
// Capture new visible columns list
Settings_SaveVisibleColumns(mi, colName);
}
#endregion "ContextMenu Methods and Events"
#region "Settings Methods"
private void Settings_SaveVisibleColumns(MenuItem mi, string colName)
{
if (theContextMenu.Items.Count - nBaseItems < dataGrid.Columns.Count)
return;
// Put the visible column names into an array
var saVisibleColumns = VisibleColumns.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries).ToList();
if (saVisibleColumns != null && saVisibleColumns.Count > 0 &&
saVisibleColumns[saVisibleColumns.Count - 1].StartsWith("-"))
saVisibleColumns.RemoveAt(saVisibleColumns.Count - 1);
// If the menu item is unchecked (column is not visible)
if (!mi.IsChecked)
// Make the column visible by adding its name to the Visible Columns list
saVisibleColumns.Add(colName);
else
// Hide the column by removing its name from the VisibleColumns list
if (saVisibleColumns.Contains(colName) && saVisibleColumns.Count > 1)
saVisibleColumns.Remove(colName);
VisibleColumns = string.Join(";", saVisibleColumns) + ";";
}
private void Settings_SaveDisplayIndexes(object sender)
{
// Capture the new column order
AllColumnDisplayIndexes = "";
foreach (DataGridColumn col in ((DataGrid)sender).Columns)
{
AllColumnDisplayIndexes +=
(AllColumnDisplayIndexes.Length > 0 ? ";" : "") + col.DisplayIndex;
}
DisplayIndexes = AllColumnDisplayIndexes;
}
#endregion "Settings Methods"
#region DisplayIndexes (DependencyProperty)
public string DisplayIndexes
{
get { return (string)GetValue(DisplayIndexesProperty); }
set { SetValue(DisplayIndexesProperty, value); }
}
public static readonly DependencyProperty DisplayIndexesProperty =
DependencyProperty.Register("DisplayIndexes", typeof(string), typeof(WpfDataGridConfigurationBehavior), new PropertyMetadata("", OnDisplayIndexesChanged));
private static void OnDisplayIndexesChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((WpfDataGridConfigurationBehavior)d).DisplayIndexesChanged(e);
}
public void DisplayIndexesChanged(DependencyPropertyChangedEventArgs e)
{
// Persist the new column order
if (dataGrid != null && !DontPersistColumnsOrder)
Settings_Save(DataGridName + "DisplayIndexes", AllColumnDisplayIndexes);
}
#endregion "DisplayIndexes (DependencyProperty)"
#region VisibleColumns (DependencyProperty)
/// <summary>
///
/// Gets or sets a value indicating the names of columns
/// (as they appear in the column header) to be visible, seperated by a semicolon.
///
/// Columns whose names are not here will be hidden.
/// </summary>
public string VisibleColumns
{
get { return (string)GetValue(VisibleColumnsProperty); }
set { SetValue(VisibleColumnsProperty, value); }
}
private void VisibleColumns_Initialize()
{
// Get saved VisibleColumns from app.config
// Initialize VisibleColumns
VisibleColumns = string.IsNullOrEmpty(VisibleColumns.Replace("\n", " ").Replace("\r", " ")) ? AllColumnsHeaders : VisibleColumns;
}
public static readonly DependencyProperty VisibleColumnsProperty =
DependencyProperty.Register("VisibleColumns", typeof(string), typeof(WpfDataGridConfigurationBehavior), new PropertyMetadata("", OnVisibleColumnsChanged));
private static void OnVisibleColumnsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((WpfDataGridConfigurationBehavior)d).VisibleColumnsChanged(e);
}
/// <summary>
///
/// Updates the display
///
/// </summary>
/// <param name="e"></param>
public void VisibleColumnsChanged(DependencyPropertyChangedEventArgs e)
{
if (theContextMenu == null)
return;
if (e.NewValue != null)
{
var showTheseColumns = e.NewValue.ToString().Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
var saContextMenuVisibleItems = new List<string>();
int iCol = 0;
foreach (MenuItem menuItem in theContextMenu.Items)
{
// Show/Hide the Context Menu item's checkmark.
if (menuItem.FontWeight == FontWeights.Bold) continue;
menuItem.IsChecked = showTheseColumns.Contains(menuItem.Header.ToString());
if (menuItem.IsChecked) saContextMenuVisibleItems.Add(menuItem.Header.ToString());
mnuShowAll.IsChecked = mnuShowAll.IsChecked && menuItem.IsChecked;
// Assign menu item's column's DisplayIndex in display order, (i.e. in menu item order), looking up each column by header name.)
if (mnuAlpha.IsChecked == false)
dataGrid.Columns.First(x => x.Header.ToString().Replace("\n", " ").Replace("\r", " ") == menuItem.Header.ToString()).DisplayIndex = iCol++;
}
// Show the columns
foreach (var col in dataGrid.Columns)
col.Visibility =
showTheseColumns.Contains(col.Header.ToString().Replace("\n", " ").Replace("\r", " "))
&& (saContextMenuVisibleItems.Contains(col.Header.ToString().Replace("\n", " ")
.Replace("\r", " ")))
? Visibility.Visible
: Visibility.Collapsed;
// Persist the new visible columns list
if (dataGrid != null && !DontPersistVisibleColumns)
Settings_Save(DataGridName + "VisibleColumns", VisibleColumns);
}
}
#endregion "VisibleColumns"
public static void Settings_Save(string propertyName, string propertyValue)
{
foreach (SettingsPropertyValue property in Settings.Default.PropertyValues)
{
if (propertyName == property.Name)
{
property.PropertyValue = propertyValue;
Settings.Default.Save();
}
}
}
}
public static class WpfDataGridConfigurationBehaviorFinder
{
public static T FindChild<T>(DependencyObject depObj) where T : DependencyObject
{
// Confirm obj is valid.
if (depObj == null) return null;
// success case
if (depObj is T) return depObj as T;
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(depObj); i++)
{
T obj = FindChild<T>(VisualTreeHelper.GetChild(depObj, i));
if (obj != null) return obj;
}
return null;
}
public interface IBreakVisualParenting
{
DependencyObject Parent { get; }
}
public static T LastVisualAncestorOfType<T>(this DependencyObject element) where T : DependencyObject
{
T item = null;
var parent = VisualTreeHelper.GetParent(element);
while (parent != null)
{
if (parent is T)
item = (T)parent;
if (parent is IBreakVisualParenting)
{
parent = ((IBreakVisualParenting)parent).Parent;
}
else
parent = VisualTreeHelper.GetParent(parent);
}
return item;
}
}
}
I'm not sure my Title is right but this is the problem I am facing now.. I have the below XAML code..
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<ComboBox ItemsSource="{Binding Path=AvailableFields}"
SelectedItem="{Binding Path=SelectedField}"
></ComboBox>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
What this basically does is, If my data source contains ten items, this is going to generate 10 row of comboboxes and all comboboxes are bounded to the same itemsource.
Now my requirement is Once an item is selected in the first combo box, that item should not be available in the subsequent combo boxes. How to satisfy this requirement in MVVM and WPF?
This turned out to be harder than I thought when I started coding it. Below sample does what you want. The comboboxes will contain all letters that are still available and not selected in another combobox.
XAML:
<Window x:Class="TestApp.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="300" Width="300">
<StackPanel>
<ItemsControl ItemsSource="{Binding Path=SelectedLetters}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<ComboBox
ItemsSource="{Binding Path=AvailableLetters}"
SelectedItem="{Binding Path=Letter}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Window>
Code behind:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Windows;
namespace TestApp
{
public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
DataContext = new VM();
}
}
public class VM : INotifyPropertyChanged
{
public VM()
{
SelectedLetters = new List<LetterItem>();
for (int i = 0; i < 10; i++)
{
LetterItem letterItem = new LetterItem();
letterItem.PropertyChanged += OnLetterItemPropertyChanged;
SelectedLetters.Add(letterItem);
}
}
public List<LetterItem> SelectedLetters { get; private set; }
private void OnLetterItemPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName != "Letter")
{
return;
}
foreach (LetterItem letterItem in SelectedLetters)
{
letterItem.RefreshAvailableLetters(SelectedLetters);
}
}
public event PropertyChangedEventHandler PropertyChanged;
public class LetterItem : INotifyPropertyChanged
{
static LetterItem()
{
_allLetters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".Select(c => c.ToString());
}
public LetterItem()
{
AvailableLetters = _allLetters;
}
public void RefreshAvailableLetters(IEnumerable<LetterItem> letterItems)
{
AvailableLetters = _allLetters.Where(c => !letterItems.Any(li => li.Letter == c) || c == Letter);
}
private IEnumerable<string> _availableLetters;
public IEnumerable<string> AvailableLetters
{
get { return _availableLetters; }
private set
{
_availableLetters = value;
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs("AvailableLetters"));
}
}
}
private string _letter;
public string Letter
{
get { return _letter; }
set
{
if (_letter == value)
{
return;
}
_letter = value;
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs("Letter"));
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
private static readonly IEnumerable<string> _allLetters;
}
}
}
This functionality is not provided by WPF, but it can be implemented using some custom coding.
I've created 3 ViewModel classes:
PreferencesVM - This will be our DataContext. It contains the master list of options which can appear in the ComboBoxes, and also contains a SelectedOptions property, which keeps track of which items are selected in the various ComboBoxes. It also has a Preferences property, which we will bind our ItemsControl.ItemsSource to.
PreferenceVM - This represents one ComboBox. It has a SelectedOption property, which ComboBox.SelectedItem is bound to. It also has a reference to PreferencesVM, and a property named Options (ComboBox.ItemsSource is bound to this), which returns the Options on PreferencesVM via a filter which checks if the item may be displayed in the ComboBox.
OptionVM - Represents a row in the ComboBox.
The following points form the key to the solution:
When PreferenceVM.SelectedOption is set (ie a ComboBoxItem is selected), the item is added to the PreferencesVM.AllOptions collection.
PreferenceVM handles Preferences.SelectedItems.CollectionChanged, and triggers a refresh by raising PropertyChanged for the Options property.
PreferenceVM.Options uses a filter to decide which items to return - which only allows items which are not in PreferencesVM.SelectedOptions, unless they are the SelectedOption.
What I've described above might be enough to get you going, but to save you the headache I'll post my code below.
PreferencesVM.cs:
public class PreferencesVM
{
public PreferencesVM()
{
PreferenceVM pref1 = new PreferenceVM(this);
PreferenceVM pref2 = new PreferenceVM(this);
PreferenceVM pref3 = new PreferenceVM(this);
this._preferences.Add(pref1);
this._preferences.Add(pref2);
this._preferences.Add(pref3);
//Only three ComboBoxes, but you can add more here.
OptionVM optRed = new OptionVM("Red");
OptionVM optGreen = new OptionVM("Green");
OptionVM optBlue = new OptionVM("Blue");
_allOptions.Add(optRed);
_allOptions.Add(optGreen);
_allOptions.Add(optBlue);
}
private ObservableCollection<OptionVM> _selectedOptions =new ObservableCollection<OptionVM>();
public ObservableCollection<OptionVM> SelectedOptions
{
get { return _selectedOptions; }
}
private ObservableCollection<OptionVM> _allOptions = new ObservableCollection<OptionVM>();
public ObservableCollection<OptionVM> AllOptions
{
get { return _allOptions; }
}
private ObservableCollection<PreferenceVM> _preferences = new ObservableCollection<PreferenceVM>();
public ObservableCollection<PreferenceVM> Preferences
{
get { return _preferences; }
}
}
PreferenceVM.cs:
public class PreferenceVM:INotifyPropertyChanged
{
private PreferencesVM _preferencesVM;
public PreferenceVM(PreferencesVM preferencesVM)
{
_preferencesVM = preferencesVM;
_preferencesVM.SelectedOptions.CollectionChanged += new NotifyCollectionChangedEventHandler(SelectedOptions_CollectionChanged);
}
void SelectedOptions_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (this.PropertyChanged != null)
this.PropertyChanged(this,new PropertyChangedEventArgs("Options"));
}
private OptionVM _selectedOption;
public OptionVM SelectedOption
{
get { return _selectedOption; }
set
{
if (value == _selectedOption)
return;
if (_selectedOption != null)
_preferencesVM.SelectedOptions.Remove(_selectedOption);
_selectedOption = value;
if (_selectedOption != null)
_preferencesVM.SelectedOptions.Add(_selectedOption);
}
}
private ObservableCollection<OptionVM> _options = new ObservableCollection<OptionVM>();
public IEnumerable<OptionVM> Options
{
get { return _preferencesVM.AllOptions.Where(x=>Filter(x)); }
}
private bool Filter(OptionVM optVM)
{
if(optVM==_selectedOption)
return true;
if(_preferencesVM.SelectedOptions.Contains(optVM))
return false;
return true;
}
public event PropertyChangedEventHandler PropertyChanged;
}
OptionVM.cs:
public class OptionVM
{
private string _name;
public string Name
{
get { return _name; }
}
public OptionVM(string name)
{
_name = name;
}
}
MainWindow.xaml.cs:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.DataContext = new PreferencesVM();
}
}
MainWindow.xaml:
<Window x:Class="WpfApplication64.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid>
<ItemsControl ItemsSource="{Binding Path=Preferences}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<ComboBox ItemsSource="{Binding Path=Options}" DisplayMemberPath="Name" SelectedItem="{Binding Path=SelectedOption}"></ComboBox>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</Window>
**Note that to reduce lines of code, my provided solution only generates 3 ComboBoxes (not 10).