I have a class called TabViewModel, and it has properties like Name, etc..
I need to be able to add tabs dynamically, and whenever a new tab is added, I need create a new instance of the TabViewModel and bind it to the new tab.
Here's my code:
XAML:
<TabControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}"/>
</DataTemplate>
</TabControl.ItemTemplate>
Code behind:
When adding a new tab..
_tabItems = new List<TabItem>();
tabItem.DataContext = ViewModel.CreateNewTabViewModel();
_tabItems.Add(tabItem);
TabControl1.ItemsSource = _tabItems;
TabControl1.SelectedIndex = 0;
So, CreateNewTabViewModel is suppose to create a new TabViewModel and set the Name property to be displayed on the tab header, which is why the TextBlock is bounded to Name.
I also tried tabItem.SetBinding but it didn't work.
Please advice.
Thanks!
_tabItems = new List<TabItem>();
//...
_tabItems.Add(tabItem);
TabControl1.ItemsSource = _tabItems;
Replaces the entire list of tab items with a new list that contains just a single tab item.
That said, the code is not quite clear on what it is doing, a lot seem unneeded. This works:
var tabItems = new List<TabViewModel>();
tabItems.Add(new TabViewModel { Name = "MyFirstTab" });
myTabControl.ItemsSource = tabItems;
myTabControl.SelectedIndex = 0;
All you need to do is add an instance of a view model to a list of view models and point the tab control to use it. There is no need to set the data context; by setting the items source you are implicitly setting the datacontext of each tab to an item in the collection.
Related
I've got a thorny problem and I'm hoping you can help. I'm creating a datagrid which involves dynamically creating columns. Here's some pseudocode for my classes:
GameLibrary
ObservableCollection<Game> Games
Game
ObservableCollection<CustomField> CustomFields
Customfield
ObservableCollection<string> Values
The datagrid is bound to a CollectionViewSource that uses GameLibrary.Games as its Source. The datagrid displays the other properties from Game in each row as I've set up the columns, and then I've got it dynamically created a column for each CustomField in CustomFields and display the relevant CustomField's Values in an itemscontrol in the cell.
This all works great, no problem. Now, though, I'm wanting to sort the Values alphabetically to display. I know best practice for this is using a CollectionViewSource, and I have managed to get one set up, attached to the DataTemplate and displaying in the itemscontrol - but it only works if, as a test, I set the CVS's source to be something external to the datagrid. This displays, but of course it displays the same thing in every row.
How do I bind the DataTemplate's CVS to something in the current row of the table? It's easy enough when not using the CVS, because I can use the binding's Path and just say "CustomFields[i].Values", but I don't know how that translates across to the CVS Source.
Here's what I have now, which works great:
FrameworkElementFactory listbox = new FrameworkElementFactory(typeof(ItemsControl));
Binding b = new Binding();
string pathb = "CustomFields[" + i + "].Values";
b.Path = new PropertyPath(pathb);
b.Mode = BindingMode.TwoWay;
b.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;
listbox.SetBinding(ItemsControl.ItemsSourceProperty, b);
listbox.SetValue(ItemsControl.PaddingProperty, new Thickness(5));
dock.AppendChild(listbox);
DataTemplate dt = new DataTemplate { VisualTree = dock };
dt.Seal();
newcolumn.CellTemplate = dt;
gameDataDisplay.Columns.Add(newcolumn);
And here's what I want:
DataTemplate dt = new DataTemplate { VisualTree = dock };
CollectionViewSource listboxCVS = new CollectionViewSource();
SortDescription listboxsortDescription = new SortDescription(".", ListSortDirection.Ascending);
listboxCVS.SortDescriptions.Add(listboxsortDescription);
listboxCVS.Source = SOMETHING HERE BUT I DONT KNOW WHAT;
dt.Resources.Add("customCVS" + i, listboxCVS);
FrameworkElementFactory listbox = new FrameworkElementFactory(typeof(ItemsControl));
Binding b = new Binding();
b.Source = listboxCVS;
listbox.SetBinding(ItemsControl.ItemsSourceProperty, b);
listbox.SetValue(ItemsControl.PaddingProperty, new Thickness(5));
dock.AppendChild(listbox);
dt.Seal();
newcolumn.CellTemplate = dt;
gameDataDisplay.Columns.Add(newcolumn);
I've also tried instead of using a CVS binding to a property in CustomFields that returns a sorted list of the Values and that displays fine, but I know it's not best practice and it doesn't update until you scroll the item offscreen and back, so I think that's a dead end.
Thank you for any help you can offer,
Tom.
PS: The ObservableCollections here aren't strictly ObservableCollections, they're a derived class with a couple extra methods, but they act exactly the same for all practical purposes. Just mentioning here for completeness.
I solved this a different way. Rather than creating a new datatemplate programmatically for each custom column, I defined a datatemplate in the window's resources then used a ContentPresenter to fill in the bindings.
XAML:
<DataTemplate x:Key="customfieldtemplate">
<DockPanel x:Name="customfieldHolder" VerticalAlignment="Center">
<DockPanel.Resources>
<CollectionViewSource x:Key="customfieldview" x:Name="customfieldview" Source="{Binding Values}">
<CollectionViewSource.SortDescriptions>
<scm:SortDescription PropertyName="."/>
</CollectionViewSource.SortDescriptions>
</CollectionViewSource>
<CollectionViewSource x:Key="customfieldsview" x:Name="customfieldsview" Source="{Binding PossibleValues}">
<CollectionViewSource.SortDescriptions>
<scm:SortDescription PropertyName="."/>
</CollectionViewSource.SortDescriptions>
</CollectionViewSource>
</DockPanel.Resources>
<customcontrols:MultiComboBox Width="17" DockPanel.Dock="Right" Margin="5,0" ShowText="False" x:Name="customfieldMiniCombo" ItemsSource="{Binding Source={StaticResource customfieldsview}}" SelectedItems="{Binding Values}" SelectionMode="Multiple" BorderBrush="{DynamicResource MahApps.Brushes.TextBox.Border}" Background="{DynamicResource MahApps.Brushes.ThemeBackground}" Foreground="{DynamicResource MahApps.Brushes.Text}"/>
<ItemsControl ItemsSource="{Binding Source={StaticResource customfieldview}}" Padding="5"/>
</DockPanel>
</DataTemplate>
Codebehind:
for (int i = 0; i < maindatafile.CurrentGameLibrary.CustomFields.Count; i++)
{
CustomColumn newcolumn = new CustomColumn();
newcolumn.Header = maindatafile.CurrentGameLibrary.CustomFields[i].Name;
gameDataDisplay.Columns.Add(newcolumn);
var template = FindResource("customfieldtemplate");
FrameworkElementFactory factory = new FrameworkElementFactory(typeof(ContentPresenter));
factory.SetValue(ContentPresenter.ContentTemplateProperty, template);
Binding newBinding = new Binding("CustomFields[" + i + "]");
newBinding.Mode = BindingMode.TwoWay;
newBinding.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;
factory.SetBinding(ContentPresenter.ContentProperty, newBinding);
DataTemplate dt = new DataTemplate { VisualTree = factory };
newcolumn.CellTemplate = dt;
}
The multicombobox user control (found here) in the template contains an "allowed" set of values to add to or remove from the observablecollection. Its ItemsSource is another property in CustomField, whose getter returns the list of allowable values for that CustomField. The multicombobox's SelectedItems property uses the same binding as the ItemsControl.
End result? The cell displays the list of values in the appropriate CustomField, with a lil dropdown button next to it. That dropdown contains all the possible values for that field, with the ones currently in the list selected. The list updates live as you select and deselect values in this combobox, and also updates live when those values are changed in other parts of the program.
I want to separate ListBox from MainMenu by creating UserControl. In MainWindow I'm creating, for testing purposes, ObservableCollection (in code-behind) and in MainWindow.xaml I'm trying to pass this Collection through DataContext to UserControl
MainWindow.xaml.cs
ObservableCollection<ListItem> coll = new ObservableCollection<ListItem>()
{
new TextListItem
{
Content = "Some Text", CreationDate = DateTime.Now, VisibleName = "Title"
}
};
MainWindow.xaml
<userControls:ListBoxUserControl DataContext="{Binding Path=coll}"/>
And in ListBoxUserControl.xaml I'm trying to access Collection like that
<ListBox x:Name="listBox" ItemsSource="{Binding}"/>
But it seems not working. How do I pass Collection through DataContext properly?
coll should be a property on the MainWindow class. As I currently read it, it is just a variable which will go out of scope after you left the method (I think the constructor) in which it is defined.
I have ComboBox in a Datagrid that I bind in a .NET WPF application as follows:
<ComboBox x:Name="categoryValues" MinWidth="70"
IsSynchronizedWithCurrentItem="False"
SelectedValuePath="Id"
ItemsSource="{Binding Source={StaticResource categoryViewSource},
Converter={StaticResource CategoriesToCategoriesConverter}}"
SelectedItem="{Binding Category,
Mode=TwoWay,
UpdateSourceTrigger=PropertyChanged,
Converter={StaticResource CategoryToCategoryConverter}}"
DisplayMemberPath="Name" SelectionChanged="categoryValues_SelectionChanged">
</ComboBox>
So each row is bound to the field Category in a different Model. The categoryViewSource is defined as follows
<CollectionViewSource x:Key="categoryViewSource" d:DesignSource="{d:DesignInstance {x:Type Models:Category}, CreateList=True}"/>
My Category Model implements INotifyPropertyChanged using PropertyChanged.Fody NuGet package. After I load the data from the database using Entity Framework, I assign the data to the CollectionViewSource as follow:
var db = new MyDbContext();
System.Windows.Data.CollectionViewSource categoryViewSource = (System.Windows.Data.CollectionViewSource)this.Resources["categoryViewSource"];
await db.Categories.LoadAsync();
categoryViewSource.Source = db.Categories.Local;
So it's bound to the Observable collection. However, once I do
var c = new Category { Name="New Category" };
db.Categories.Add(c);
my CollectionViewSource is updated (at least in the debugger), which seems logical, it's source is an ObservableCollection. But even if I do
categoryViewSource.View.Refresh();
my ComboBox ItemsSource isn't updated. I tried with IsSynchronizedWithCurrentItem="True" as seen somewhere on StackOverflow. So far it only works, if I do something like this:
categoryViewSource.Source = null;
categoryViewSource.Source = db.Categories.Local;
But then my SelectedItem property is null in all ComboBoxes and they appear empty in the Datagrid because apparently the instances of SelectedItem are different once you reassign the collection.
Does anyone have a solution. Unfortunately, I'm still really new to WPF and have no idea.
Adding a new Category to the DbSet<Category> doesn't affect the ObservableCollection<Category>. These are two different and independent in-memory collections. You will have to add the same object to both Collections.
You could create an ObservableCollection and populate this one with your entity objects when you initialize your ComboBox. And then you add any new objects to both your context and to your data-bound collection, i.e.:
public partial class MainWindow : Window
{
ObservableCollection<Category> _cats;
public MainWindow()
{
InitializeComponent();
var db = new MyDbContext();
System.Windows.Data.CollectionViewSource categoryViewSource = (System.Windows.Data.CollectionViewSource)this.Resources["categoryViewSource"];
await db.Categories.LoadAsync();
_cats = new ObservableCollection<Category>(db.Categories.Local);
categoryViewSource.Source = _cats;
//...
var c = new Category { Name = "New Category" };
db.Categories.Add(c); //add to DbSet
_cats.Add(c); //add to ObservableCollection
}
}
Ok, if I add it directly as ItemsSource, that does work. However if I add the collection through a separate Binding, it doesn't work. I need to pass the collection through the converter though. Is there a way to do it automatically?
You could set up the binding programmatically:
categoryValues.SetBinding(ComboBox.ItemsSourceProperty, new Binding(".") { Source = _cats, Converter = new CategoriesToCategoriesConverter() });
I'm looking for the best way to populate a check boxes from the following code. I have looked into Binding but not really sure where to go.
Here is the edited code that is working
private void dpDateSelection_SelectedDateChanged(object sender, SelectionChangedEventArgs e)
{
DateTime? date = dpDateSelection.SelectedDate;
logDate = date != null ? date.Value.ToString("yyyy-MM-dd") : null;
dpDateSelection.ToolTip = logDate;
LoadLogs(logDate);
}
private void LoadLogs(string ldate)
{
string[] logs = Directory.GetFiles(logPath + ldate, "*.ininlog");
InitializeComponent();
logList = new ObservableCollection<String>();
logList.Clear();
foreach (string logName in logs)
{
string s = logName.Substring(logName.IndexOf(ldate) + ldate.Length + 1);
int extPos = s.LastIndexOf(".");
s = s.Substring(0, extPos);
logList.Add(s);
}
this.DataContext = this;
}
<ListBox x:Name="Logs" ItemsSource="{Binding logList}">
<ListBox.ItemTemplate>
<DataTemplate>
<CheckBox Content="{Binding}" ToolTip="{Binding}" Tag="{Binding}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
You will want to start by using an ItemsControl instead of a StackPanel, since ItemsControls are automatically set up to display collections of things:
<ItemsControl ItemsSource="{Binding Logs}"/>
Note the use of ItemsSource. With the accompanying binding string, it basically says "Look for a property on the DataContext called "Logs" and put everything in it into this control".
Next you said you wanted this displayed as checkboxes, so we use an item template:
<ItemsControl ItemsSource="{Binding Logs}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<CheckBox Content={Binding .}/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
This says "Use a checkbox for each Item in the ItemsSource". The DataTemplate can be a Grid or other collection control as well, so this is a really powerful feature in WPF. The "Binding ." just binds to the object itself (a string in this case, so we don't need a special path).
Finally, you need to set up a property to bind to in your view model:
ObservableCollection<String> Logs {get; set;}
You want an ObservableCollection so that when anything is added to or removed from the list, it automatically updates the UI. If you are going to be completely replacing the list (assignment), then you need to implement INotifyPropertyChanged and invoke the PropertyChanged event in that properties setter.
In your posted loop, you would add each log file to this property.
Also, make sure that somewhere you set the DataContext property of the XAML file (View) to your view model object. If everything is in code behind, use DataContext = this. Note that doing this is considered bad practice, and you should use a separate class (ViewModel).
You didn't mention what you wanted the CheckBoxes to do, so I haven't included anything related to that in my answer. You will likely want to abstract your logs into an object with a "Selected" property you can then bind the IsChecked property of the CheckBoxes to.
Obviously this is a lot to take in, please let me know if I can clarify anything or help further!
Update
You put the property in your ViewModel (DataContext). Whatever class that is, you write:
ObservableCollection<String> Logs {get; set;}
private void LoadLogs()
{
string[] logs = Directory.GetFiles(logPath + logDate, "*.ininlog");
foreach(string logName in logs)
{
string s = logName.Substring(logName.IndexOf(logDate) + logDate.Length + 1);
int extPos = s.LastIndexOf(".");
s = s.Substring(0, extPos);
//MessageBox.Show(s);
Logs.Add(s); //Add the parsed file name to the list
}
}
I have a button, when pressed it adds a textbox and a listbox to a stackpanel and adds this stackpanel to another stackpanel named "stackPanelAdd". Just like this:
private void buttonAdd_Click(object sender, RoutedEventArgs e)
{
StackPanel sp = new StackPanel();
TextBox tb = new TextBox();
ListBox lb = new ListBox();
tb.Margin = new Thickness(5, 5, 5, 0);
lb.Margin = new Thickness(5, 5, 5, 0);
lb.Height = 200;
sp.Children.Add(tb);
sp.Children.Add(lb);
stackPanelAdd.Children.Add(sp);
}
How do I remove the last children in the stackpanel "stackPanelAdd"?
Should I use something like stackPanelAdd.children.Remove? if so then how do i get the last element in the stackpanel?
Try:
if (stackPanelAdd.Children.Count>0)
{
stackPanelAdd.Children.RemoveAt(stackPanelAdd.Children.Count-1);
}
That is not a good idea, if you stick to this method things will probably get very messy sooner or later. When dealing with items that can be added and removed in WPF you will want to use an ItemsControl of some kind on top of panels (you can change the panel using the ItemsPanel property, by default it will be a StackPanel).
The creation of the controls can also be improved by using data templates and data binding which are core mechanisms that you should become familiar with.
An example:
<ItemsControl ItemsSource="{Binding Data}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBox Text="{Binding Name}" Margin="5,5,5,0"/>
<ListBox ItemsSource="{Binding Items}" Margin="5,5,5,0" Height="200"/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
Here Data is a source collection which should implement INotifyCollectionChanged, then you can just remove an item from that collection and its corresponding StackPanel will be gone. The items in Data should contain the bound properties Name and Items which you then can assign values to or get entered text from (the class should implement INPC, read more about those things in the article on data binding).
You can use
var lastControl = stackPanelAdd.Children.LastOrDefault();
//Last is defined in System.Linq.Enumrable
if(lastControl != null)
stackPanelAdd.Children.Remove(lastControl);
#Milan Halada's answer worked for me with a little change,
while (stackPanelAdd.Children.Count>0)
{
stackPanelAdd.Children.RemoveAt(stackPanelAdd.Children.Count-1);
}
so, it removes all the children and then i add new children to it dynamically using for loop, with new data.