I have a button with the following content:
<StackPanel Orientation="Horizontal">
<TextBlock Text="Connect"/>
<materialDesign:PackIcon Kind="Arrow"/>
</StackPanel>
I searched and found this: WPF Button content binding but I'm not sure how to apply the solution when I have all of the three: a Stackpanel, the PackIcon (object) and the Textblock.
I have this progressBar which I make it appear under the button:
<ProgressBar x:Name="XZ" Foreground="Black" Grid.Row="4" Grid.Column="1"
Visibility="{Binding Connecting, UpdateSourceTrigger=PropertyChanged, Mode=OneWay, Converter={StaticResource BooleanToVisibilityConverter}}"
Value="50"
IsIndeterminate="True" />
I want to make it so when I click the button, instead of showing the ProgressBar where it is right now, to basically remove the Text and the PackIcon and place the ProgressBar in the button.
Actually changing in the controls could be done with Data Triggers; though that seems a bit over the top in this case.
I would just toggle the visibility of two controls:
<Grid>
<StackPanel Orientation="Horizontal" Visibility="{Binding Connecting, Converter={StaticResource BooleanToCollapsedConverter}}"">
<TextBlock Text="Connect"/>
<materialDesign:PackIcon Kind="Arrow"/>
</StackPanel>
<ProgressBar x:Name="XZ" Foreground="Black" Grid.Row="4" Grid.Column="1"
Visibility="{Binding Connecting, Converter={StaticResource BooleanToVisibilityConverter}}"
Value="50"
IsIndeterminate="True" />
</Grid>
That would be the content of your button. BooleanToCollapsedConverter is just the inverse of a VisibiltyToBooleanConverter; there are a number of ways to do it and is left as an exercise.
As an aside; UpdateSourceTrigger doesn't make any sense on a OneWay binding (it doesn't update the source!) and you don't even need that on visibility as that's not an input the user can change.
You could use a data template. Something like:
XAML:
<Window.Resources>
<DataTemplate DataType="{x:Type local:ButtonInfo}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
</Grid.RowDefinitions>
<Label Grid.Row="0" Content="Press me"></Label>
<Label Grid.Row="1" Content="{Binding Label}"></Label>
</Grid>
</DataTemplate>
<DataTemplate DataType="{x:Type local:ProgressInfo}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
</Grid.RowDefinitions>
<ProgressBar Height="30" Value="{Binding Progress}"></ProgressBar>
<Label Grid.Row="1" Content="{Binding Label}"></Label>
</Grid>
</DataTemplate>
</Window.Resources>
<Grid>
<Button Command="{Binding ProcessCommand}" Content="{Binding ButtonInfo}">
</Button>
</Grid>
C#:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.DataContext = new MainWindowViewModel();
}
}
public class ViewModelBase:INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
public class MainWindowViewModel:ViewModelBase
{
public MainWindowViewModel()
{
ButtonInfo = new ButtonInfo(){Label = "Button Info"};
ProcessCommand = new DelegateCommand(Process);
}
private ButtonInfo _buttonInfo;
public ButtonInfo ButtonInfo
{
get { return _buttonInfo; }
set
{
_buttonInfo = value;
OnPropertyChanged();
}
}
public DelegateCommand ProcessCommand { get; set; }
private async void Process()
{
ButtonInfo = new ProgressInfo(){Label = "Progress Info"};
await ProcessAsync();
}
private Task ProcessAsync()
{
return Task.Run(() =>
{
for (int i = 0; i < 100; i++)
{
Application.Current.Dispatcher.Invoke(() =>
{
ButtonInfo.Progress = i;
if (i==99)
{
ButtonInfo = new ButtonInfo(){Label = "Button Again"};
}
});
Thread.Sleep(100);
}
});
}
}
public class ButtonInfo:ViewModelBase
{
private string _label;
private int _progress;
private bool _isProcessing;
public string Label
{
get { return _label; }
set
{
_label = value;
OnPropertyChanged();
}
}
public int Progress
{
get { return _progress; }
set
{
_progress = value;
OnPropertyChanged();
}
}
public bool IsProcessing
{
get { return _isProcessing; }
set
{
_isProcessing = value;
OnPropertyChanged();
}
}
}
public class ProgressInfo : ButtonInfo { }
You can create a template for the button to achieve this, then reuse the template everywhere you want a button with loading as following:
<Button Width="120" Height="40" Tag="False" Name="loadingButton" Click="loadingButton_Click">
<Button.Template>
<ControlTemplate>
<Border Name="PART_Border" BorderBrush="Black" BorderThickness="1" CornerRadius="2" Background="Transparent">
<Grid Name="PART_Root">
<TextBlock Name="PART_Text" HorizontalAlignment="Center" VerticalAlignment="Center">Data</TextBlock>
<ProgressBar IsIndeterminate="True" Name="PART_Loading"></ProgressBar>
</Grid>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="Tag" Value="True">
<Setter TargetName="PART_Text" Property="Visibility" Value="Collapsed"></Setter>
<Setter TargetName="PART_Loading" Property="Visibility" Value="Visible"></Setter>
</Trigger>
<Trigger Property="Tag" Value="False" >
<Setter TargetName="PART_Text" Property="Visibility" Value="Visible"></Setter>
<Setter TargetName="PART_Loading" Property="Visibility" Value="Collapsed"></Setter>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Button.Template>
</Button>
And the event for button click would be:
private async void loadingButton_Click(object sender, RoutedEventArgs e)
{
loadingButton.Tag = true.ToString();//display loading
await Task.Run(() => { Thread.Sleep(4000); });//fake data loading
loadingButton.Tag = false.ToString();//hide loading
}
Note that you can also bind the Tag property for a property inside your view model if you where using MVVM pattern.
Have style for a series of buttons btn1, btn2, btn3, etc.
Inside the style for each button is a TextBlock for displaying the "Content" of the button, (since the border inside the style covers any content of the button itself).
Now, I would like for the TextBlock name to be tied to the button name. For example - btn1's text block's name would be btn1Txt. The purpose of this would be the end user can assign each button its own text in a settings menu.
Any hints on how I would go about this? I admit I'm relatively new to WPF and bindings.
EDIT:::: WHAT I"VE GOT SO FAR THAT IS WORKING.
On load, the program checks the settings file for the Text for each button. Each button's content is assigned the proper information. Then inside the style, I bind the TextBlock Text to the content of the parent button.
This may not be the normal way of going about it, but it works
Method
List<string> MainButtons = Properties.Settings.Default.MainButtonNames.Cast<string>().ToList();
for (int i = 0; i < MainButtons.Count(); i++)
{
string actualNum = Convert.ToString((i + 1));
var MainButtonFinder = (Button)this.FindName("MainButton" + actualNum);
Console.WriteLine(MainButtonFinder.Name);
MainButtonFinder.Content = MainButtons[i];
Console.WriteLine(MainButtonFinder.Content);
}
Style
<Style x:Key="MainButtonStyle" TargetType="{x:Type Button}">
<Setter Property="HorizontalAlignment" Value="Stretch"/>
<Setter Property="Width" Value="100px"/>
<Setter Property="MinHeight" Value="50"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border CornerRadius="20" Height="45" Width="100" Margin="0" Background="#FF99CCFF">
<TextBlock Text="{Binding RelativeSource={RelativeSource Mode=TemplatedParent}, Path=Content}" HorizontalAlignment="Center" VerticalAlignment="Center" FontFamily="LCARS" Foreground="White" Padding="5px" FontSize="18px" />
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>`
This is the wrong way to go about what you're trying to do. Here's the "right" way to do it. There's a fair amount of boilerplate code here, but you get used to it.
Write a button viewmodel and give your main viewmodel an ObservableCollection of those:
#region ViewModelBase Class
public class ViewModelBase : INotifyPropertyChanged
{
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([System.Runtime.CompilerServices.CallerMemberName] string propName = null) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propName));
#endregion INotifyPropertyChanged
}
#endregion ViewModelBase Class
#region MainViewModel Class
public class MainViewModel : ViewModelBase
{
public MainViewModel()
{
ButtonItems.Add(new ButtonItemViewModel("First Command", "First Item", () => MessageBox.Show("First Item Executed")));
ButtonItems.Add(new ButtonItemViewModel("Second Command", "Second Item", () => MessageBox.Show("Second Item Executed")));
}
#region ButtonItems Property
public ObservableCollection<ButtonItemViewModel> ButtonItems { get; }
= new ObservableCollection<ButtonItemViewModel>();
#endregion ButtonItems Property
}
#endregion MainViewModel Class
#region ButtonItemViewModel Class
public class ButtonItemViewModel : ViewModelBase
{
public ButtonItemViewModel(String cmdName, String text, Action cmdAction)
{
CommandName = cmdName;
Text = text;
Command = new DelegateCommand(cmdAction);
}
#region Text Property
private String _text = default(String);
public String Text
{
get { return _text; }
set
{
if (value != _text)
{
_text = value;
OnPropertyChanged();
}
}
}
#endregion Text Property
#region CommandName Property
private String _commandName = default(String);
public String CommandName
{
get { return _commandName; }
private set
{
if (value != _commandName)
{
_commandName = value;
OnPropertyChanged();
}
}
}
#endregion CommandName Property
public ICommand Command { get; private set; }
}
#endregion ButtonItemViewModel Class
public class DelegateCommand : ICommand
{
public DelegateCommand(Action action)
{
_action = action;
}
private Action _action;
public event EventHandler CanExecuteChanged;
public bool CanExecute(object parameter)
{
return true;
}
public void Execute(object parameter)
{
_action?.Invoke();
}
}
Make that MainViewModel the DataContext of your Window:
public MainWindow()
{
InitializeComponent();
DataContext = new MainViewModel();
}
And here's how you can put it all together in the XAML:
<Grid>
<StackPanel Orientation="Vertical">
<GroupBox Header="Buttons">
<ItemsControl ItemsSource="{Binding ButtonItems}" HorizontalAlignment="Left">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button
Margin="2"
MinWidth="80"
Content="{Binding Text}"
Command="{Binding Command}"
/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</GroupBox>
<GroupBox Header="Edit Buttons">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<ListBox
Grid.Column="0"
Margin="2"
x:Name="ButtonEditorListBox"
ItemsSource="{Binding ButtonItems}"
HorizontalAlignment="Left"
>
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock
Margin="2"
Text="{Binding CommandName}"
/>
<TextBlock
Margin="2"
Text="{Binding Text, StringFormat=': "{0}"'}"
/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ListBox>
<ContentControl
Grid.Column="1"
Margin="8,2,2,2"
Content="{Binding SelectedItem, ElementName=ButtonEditorListBox}"
>
<ContentControl.ContentTemplate>
<DataTemplate>
<StackPanel Orientation="Vertical">
<TextBlock
Margin="2"
HorizontalAlignment="Stretch"
FontWeight="Bold"
Text="{Binding CommandName, StringFormat={}{0}: }"
/>
<TextBox
Margin="2"
HorizontalAlignment="Stretch"
Text="{Binding Text, UpdateSourceTrigger=PropertyChanged}"
/>
</StackPanel>
</DataTemplate>
</ContentControl.ContentTemplate>
</ContentControl>
</Grid>
</GroupBox>
</StackPanel>
</Grid>
Back to your question:
Inside the style for each button is a TextBlock for displaying the "Content" of the button, (since the border inside the style covers any content of the button itself).
You're doing styles wrong. Very, very wrong. I can help you fix it if you show me the style.
As I understand what you want is to modify the "text" of the button, it occurs to me that you can do it this way.
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="20"></RowDefinition>
<RowDefinition></RowDefinition>
<RowDefinition></RowDefinition>
</Grid.RowDefinitions>
<TextBox Text="{Binding ElementName=ButtonTest, Path=Content, UpdateSourceTrigger=PropertyChanged}" ></TextBox>
<Button Name="ButtonTest" Grid.Row="1" Width="100" Height="40">
<Button.ContentTemplate>
<DataTemplate>
<TextBlock Foreground="Blue" Text="{Binding}"></TextBlock>
</DataTemplate>
</Button.ContentTemplate>
</Button>
</Grid>
I'm in search of some help. I've created a very basic MVVM setup. My object is called VNode which has the properties Name,Age,Kids. What I want to happen is when the user selects VNodes on the left, it displays their more in depth data on the right as scene in the image below. I'm not sure how to go about doing this.
image 1: Current
Image 2: Goal
If you don't feel like using the code below to recreate the window you can grab the project solution files from here: DropboxFiles
VNode.cs
namespace WpfApplication1
{
public class VNode
{
public string Name { get; set; }
public int Age { get; set; }
public int Kids { get; set; }
}
}
MainWindow.xaml
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApplication1"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<local:MainViewModel/>
</Window.DataContext>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="8" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<ListBox Grid.Column="0" Background="AliceBlue" ItemsSource="{Binding VNodes}" SelectionMode="Extended">
<ListBox.ItemTemplate>
<DataTemplate>
<WrapPanel>
<TextBlock Text="Name: " />
<TextBlock Text="{Binding Name}" FontWeight="Bold" />
</WrapPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<GridSplitter Grid.Column="1" Width="5" HorizontalAlignment="Stretch" />
<ListBox Grid.Column="2" Background="LightBlue" ItemsSource="{Binding VNodes}">
<ListBox.ItemTemplate>
<DataTemplate>
<WrapPanel>
<TextBlock Text="{Binding Name}" FontWeight="Bold" />
<TextBlock Text=":" FontWeight="Bold" />
<TextBlock Text=" age:"/>
<TextBlock Text="{Binding Age}" FontWeight="Bold" />
<TextBlock Text=" kids:"/>
<TextBlock Text="{Binding Kids}" FontWeight="Bold" />
</WrapPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</Window>
MainViewModel.cs
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace WpfApplication1
{
public class MainViewModel : ObservableObject
{
private ObservableCollection<VNode> _vnodes;
public ObservableCollection<VNode> VNodes
{
get { return _vnodes; }
set
{
_vnodes = value;
NotifyPropertyChanged("VNodes");
}
}
Random r = new Random();
public MainViewModel()
{
//hard coded data for testing
VNodes = new ObservableCollection<VNode>();
List<string> names = new List<string>() { "Tammy", "Doug", "Jeff", "Greg", "Kris", "Mike", "Joey", "Leslie", "Emily","Tom" };
List<int> ages = new List<int>() { 32, 24, 42, 57, 17, 73, 12, 8, 29, 31 };
for (int i = 0; i < 10; i++)
{
VNode item = new VNode();
int x = r.Next(0,9);
item.Name = names[x];
item.Age = ages[x];
item.Kids = r.Next(1, 5);
VNodes.Add(item);
}
}
}
}
ObservableObject.cs
using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace WpfApplication1
{
public class ObservableObject : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public void NotifyPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
}
UPDATED
For the sake of example, how about demonstrating if the user just selects a single item in the ListBox on the right, it then displays that selected items more in-depth data on the right as shown in the image below?
There are three and a half answers here. Number one is good general WPF practice that doesn't work in the specific case of ListBox. The second one is a quick and dirty workaround for the problem with ListBox, and the last is the best, because it does nothing in code behind. Least code behind is best code behind.
The first way to do this doesn't require anything of the items you're displaying in the ListBox. They could be strings or integers. If your item type (or types) is a class (or are classes) with a little more meat to it, and you'd like to have each instance know whether it's been selected or not, we'll get to that next.
You need to give your view model another ObservableCollection<VNode> called SelectedVNodes or some such.
private ObservableCollection<VNode> _selectedvnodes;
public ObservableCollection<VNode> SelectedVNodes
{
get { return _selectedvnodes; }
set
{
_selectedvnodes = value;
NotifyPropertyChanged("SelectedVNodes");
}
}
public MainViewModel()
{
VNodes = new ObservableCollection<VNode>();
SelectedVNodes = new ObservableCollection<VNode>();
// ...etc., just as you have it now.
If System.Windows.Controls.ListBox weren't broken, then in your first ListBox, you would bind SelectedItems to that viewmodel property:
<ListBox
Grid.Column="0"
Background="AliceBlue"
ItemsSource="{Binding VNodes}"
SelectedItems="{Binding SelectedVNodes}"
SelectionMode="Extended">
And the control would be in charge of the content of SelectedVNodes. You could also change SelectedVNodes programmatically, and that would update both lists.
But System.Windows.Controls.ListBox is broken, and you can't bind anything to SelectedItems. The simplest workaround is to handle the ListBox's SelectionChanged event and kludge it in the code behind:
XAML:
<ListBox
Grid.Column="0"
Background="AliceBlue"
ItemsSource="{Binding VNodes}"
SelectionMode="Extended"
SelectionChanged="ListBox_SelectionChanged">
C#:
private void ListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
ListBox lb = sender as ListBox;
MainViewModel vm = DataContext as MainViewModel;
vm.SelectedVNodes.Clear();
foreach (VNode item in lb.SelectedItems)
{
vm.SelectedVNodes.Add(item);
}
}
Then bind ItemsSource in your second ListBox to SelectedVNodes:
<ListBox
Grid.Column="2"
Background="LightBlue"
ItemsSource="{Binding SelectedVNodes}">
And that should do what you want. If you want to be able to update SelectedVNodes programmatically and have the changes reflected in both lists, you'll have to have your codebehind class handle the PropertyChanged event on the viewmodel (set that up in the codebehind's DataContextChanged event), and the CollectionChanged event on viewmodel.SelectedVNodes -- and remember to set the CollectionChanged handler all over again every time SelectedVNodes changes its own value. It gets ugly.
A better long-term solution would be to write an attachment property for ListBox that replaces SelectedItems and works right. But this kludge will at least get you moving for the time being.
Update
Here's a second way of doing it, which OP suggested. Instead of maintaining a selected item collection, we put a flag on each item, and the viewmodel has a filtered version of the main item list that returns only selected items. I'm drawing a blank on how to bind VNode.IsSelected to the IsSelected property on ListBoxItem, so I just did that in the code behind.
VNode.cs:
using System;
namespace WpfApplication1
{
public class VNode
{
public string Name { get; set; }
public int Age { get; set; }
public int Kids { get; set; }
// A more beautiful way to do this would be to write an IVNodeParent
// interface with a single method that its children would call
// when their IsSelected property changed -- thus parents would
// implement that, and they could name their "selected children"
// collection properties anything they like.
public ObservableObject Parent { get; set; }
private bool _isSelected = false;
public bool IsSelected
{
get { return _isSelected; }
set
{
if (value != _isSelected)
{
_isSelected = value;
if (null == Parent)
{
throw new NullReferenceException("VNode.Parent must not be null");
}
Parent.NotifyPropertyChanged("SelectedVNodes");
}
}
}
}
}
MainViewModel.cs:
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace WpfApplication1
{
public class MainViewModel : ObservableObject
{
private ObservableCollection<VNode> _vnodes;
public ObservableCollection<VNode> VNodes
{
get { return _vnodes; }
set
{
_vnodes = value;
NotifyPropertyChanged("VNodes");
NotifyPropertyChanged("SelectedVNodes");
}
}
public IEnumerable<VNode> SelectedVNodes
{
get { return _vnodes.Where(vn => vn.IsSelected); }
}
Random r = new Random();
public MainViewModel()
{
//hard coded data for testing
VNodes = new ObservableCollection<VNode>();
List<string> names = new List<string>() { "Tammy", "Doug", "Jeff", "Greg", "Kris", "Mike", "Joey", "Leslie", "Emily","Tom" };
List<int> ages = new List<int>() { 32, 24, 42, 57, 17, 73, 12, 8, 29, 31 };
for (int i = 0; i < 10; i++)
{
VNode item = new VNode();
int x = r.Next(0,9);
item.Name = names[x];
item.Age = ages[x];
item.Kids = r.Next(1, 5);
item.Parent = this;
VNodes.Add(item);
}
}
}
}
MainWindow.xaml.cs:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace WpfApplication1
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void ListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
foreach (VNode item in e.RemovedItems)
{
item.IsSelected = false;
}
foreach (VNode item in e.AddedItems)
{
item.IsSelected = true;
}
}
}
}
MainWindow.xaml (partial):
<ListBox
Grid.Column="0"
Background="AliceBlue"
ItemsSource="{Binding VNodes}"
SelectionMode="Extended"
SelectionChanged="ListBox_SelectionChanged">
<ListBox.ItemTemplate>
<DataTemplate>
<WrapPanel>
<TextBlock Text="Name: " />
<TextBlock Text="{Binding Name}" FontWeight="Bold" />
</WrapPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<GridSplitter Grid.Column="1" Width="5" HorizontalAlignment="Stretch" />
<ListBox Grid.Column="2" Background="LightBlue" ItemsSource="{Binding SelectedVNodes}">
<ListBox.ItemTemplate>
<DataTemplate>
<WrapPanel>
<TextBlock Text="{Binding Name}" FontWeight="Bold" />
<TextBlock Text=":" FontWeight="Bold" />
<TextBlock Text=" age:"/>
<TextBlock Text="{Binding Age}" FontWeight="Bold" />
<TextBlock Text=" kids:"/>
<TextBlock Text="{Binding Kids}" FontWeight="Bold" />
</WrapPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Update 2
And here, finally, is how you do it with binding (thanks to OP for figuring out for me how to bind data item properties to ListBoxItem properties -- I should be able to accept his comment as an answer!):
In MainWindow.xaml, get rid of the SelectionCanged event (yay!), and set a Style to do the binding only on the items in the first ListBox. In the second ListBox, that binding will create problems which I'll leave to somebody else to resolve; I have a guess that it might be fixable by fiddling with the order of notifications and assignments in VNode.IsSelected.set, but I could be wildly wrong about that. Anyway the binding serves no purpose in the second ListBox so there's no reason to have it there.
<ListBox
Grid.Column="0"
Background="AliceBlue"
ItemsSource="{Binding VNodes}"
SelectionMode="Extended"
>
<ListBox.Resources>
<Style TargetType="{x:Type ListBoxItem}" BasedOn="{StaticResource {x:Type ListBoxItem}}">
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
</Style>
</ListBox.Resources>
<ListBox.ItemTemplate>
<DataTemplate>
<WrapPanel>
<TextBlock Text="Name: " />
<TextBlock Text="{Binding Name}" FontWeight="Bold" />
</WrapPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
...and I removed the event handler method from the codebehind. But you didn't add it at all, because you're smarter than me and you started with this last version of the answer.
In VNode.cs, VNode becomes an ObservableObject so he can advertise his selection status, and he also fires the appropriate notification in IsSelected.set. He still has to fire the change notification for his Parent's SelectedVNodes property, because the second listbox (or any other consumer of SelectedVNodes) needs to know that the set of selected VNodes has changed.
Another way to do that would be to make SelectedVNodes an ObservableCollection again, and have VNode add/remove himself from it when his selected status changes. Then the viewmodel would have to handle CollectionChanged events on that collection, and update the VNode IsSelected properties when they're added to it or removed from it. If you do that, it's very important to keep the if in VNode.IsSelected.set, to prevent infinite recursion.
using System;
namespace WpfApplication1
{
public class VNode : ObservableObject
{
public string Name { get; set; }
public int Age { get; set; }
public int Kids { get; set; }
public ObservableObject Parent { get; set; }
private bool _isSelected = false;
public bool IsSelected
{
get { return _isSelected; }
set
{
if (value != _isSelected)
{
_isSelected = value;
if (null == Parent)
{
throw new NullReferenceException("VNode.Parent must not be null");
}
Parent.NotifyPropertyChanged("SelectedVNodes");
NotifyPropertyChanged("IsSelected");
}
}
}
}
}
Update 3
OP asks about displaying a single selection in a detail pane. I left the old multi-detail pane in place to demonstrate sharing a template.
That's pretty simple to do, so I elaborated a bit. You could do this only in the XAML, but I threw in a SelectedVNode property in the viewmodel to demonstrate that as well. It's not used for anything, but if you wanted to throw in a command that operated on the selected item (for example), that's how the view model would know which item the user means.
MainViewModel.cs
// Add to MainViewModle class
private VNode _selectedVNode = null;
public VNode SelectedVNode
{
get { return _selectedVNode; }
set
{
if (value != _selectedVNode)
{
_selectedVNode = value;
NotifyPropertyChanged("SelectedVNode");
}
}
}
MainWindow.xaml:
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApplication1"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<SolidColorBrush x:Key="ListBackgroundBrush" Color="Ivory" />
<DataTemplate x:Key="VNodeCardTemplate">
<Grid>
<Border
x:Name="BackgroundBorder"
BorderThickness="1"
BorderBrush="Silver"
CornerRadius="16,6,6,6"
Background="White"
Padding="6"
Margin="4,4,8,8"
>
<Border.Effect>
<DropShadowEffect BlurRadius="2" Opacity="0.25" ShadowDepth="4" />
</Border.Effect>
<Grid
x:Name="ContentGrid"
>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<!-- Each gets half of what's left -->
<ColumnDefinition Width="0.5*" />
<ColumnDefinition Width="0.5*" />
</Grid.ColumnDefinitions>
<Border
Grid.Row="0" Grid.RowSpan="3"
VerticalAlignment="Top"
Grid.Column="0"
BorderBrush="{Binding Path=BorderBrush, ElementName=BackgroundBorder}"
BorderThickness="1"
CornerRadius="9,4,4,4"
Margin="2,2,6,2"
Padding="4"
>
<StackPanel Orientation="Vertical">
<StackPanel.Effect>
<DropShadowEffect BlurRadius="2" Opacity="0.25" ShadowDepth="2" />
</StackPanel.Effect>
<Ellipse
Width="16" Height="16"
Fill="DarkOliveGreen"
Margin="0,0,0,2"
HorizontalAlignment="Center"
/>
<Border
CornerRadius="6,6,2,2"
Background="DarkOliveGreen"
Width="36"
Height="18"
Margin="0"
/>
</StackPanel>
</Border>
<TextBlock Grid.Row="0" Grid.Column="1" Grid.ColumnSpan="2" Text="{Binding Name}" FontWeight="Bold" />
<Separator Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="2" Background="{Binding Path=BorderBrush, ElementName=BackgroundBorder}" Margin="0,3,0,3" />
<!--
Mode=OneWay on Run.Text because bindings on that property should default to that, but don't.
And if you bind TwoWay to a property without a setter, it throws an exception.
-->
<TextBlock Grid.Row="2" Grid.Column="1"><Bold>Age:</Bold> <Run Text="{Binding Age, Mode=OneWay}" /></TextBlock>
<TextBlock Grid.Row="2" Grid.Column="2"><Bold>Kids:</Bold> <Run Text="{Binding Kids, Mode=OneWay}" /></TextBlock>
</Grid>
</Border>
</Grid>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding}" Value="{x:Null}">
<Setter TargetName="ContentGrid" Property="Visibility" Value="Hidden" />
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
<Style TargetType="{x:Type ListBoxItem}" BasedOn="{StaticResource {x:Type ListBoxItem}}">
<!-- I think this should be the default, but it isn't. -->
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
</Style>
</Window.Resources>
<Window.DataContext>
<local:MainViewModel/>
</Window.DataContext>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="8" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="0.5*" />
<RowDefinition Height="0.5*" />
</Grid.RowDefinitions>
<ListBox
x:Name="VNodeMasterList"
Grid.Column="0"
Grid.Row="0"
Grid.RowSpan="2"
Background="{StaticResource ListBackgroundBrush}"
ItemsSource="{Binding VNodes}"
SelectionMode="Extended"
SelectedItem="{Binding SelectedVNode}"
>
<ListBox.Resources>
<Style TargetType="{x:Type ListBoxItem}" BasedOn="{StaticResource {x:Type ListBoxItem}}">
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
</Style>
</ListBox.Resources>
<ListBox.ItemTemplate>
<DataTemplate>
<WrapPanel>
<TextBlock Text="Name: " />
<TextBlock Text="{Binding Name}" FontWeight="Bold" />
</WrapPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<GridSplitter Grid.Column="1" Grid.RowSpan="2" Grid.Row="0" Width="5" HorizontalAlignment="Stretch" />
<Border
Grid.Column="2"
Grid.Row="0"
Background="{StaticResource ListBackgroundBrush}"
>
<ContentControl
Content="{Binding ElementName=VNodeMasterList, Path=SelectedItem}"
ContentTemplate="{StaticResource VNodeCardTemplate}"
/>
</Border>
<ListBox
Grid.Column="2"
Grid.Row="1"
Background="{StaticResource ListBackgroundBrush}"
ItemsSource="{Binding SelectedVNodes}"
ItemTemplate="{StaticResource VNodeCardTemplate}"
/>
</Grid>
</Window>
Imagine multiple object types in one Listbox. Both have different look and presented information. If I add any of them in ObservableCollection it displays them fine in ListBox.
<DataTemplate DataType="{x:Type local:DogData}" >
<Grid...
</DataTemplate>
<DataTemplate DataType="{x:Type local:CatData}" >
<Grid...
</DataTemplate>
Now I want users to be able to press a button and switch view to see more detailed information and there templates which provide it
<DataTemplate x:Key="TemplateDogDataWithImages" >
<Grid...
</DataTemplate>
<DataTemplate x:Key="TemplateCatDataWithImages" >
<Grid...
</DataTemplate>
but I can assign only one
AnimalsListBox.ItemTemplate = this.Resources["TemplateDogDataWithImages"] as DataTemplate;
I don't want to have one and have a bunch of triggers in it.
I have researched DataTemplateSelectors
http://tech.pro/tutorial/807/wpf-tutorial-how-to-use-a-datatemplateselector
Is there a better way? Is there a way to switch default templates for each data type so I could avoid DataTemplateSelectors?
On click of the button how do I select TemplateDogDataWithImages and TemplateCatDataWithImages as default ones and cliking other button use TemplateDogDataSimple and TemplateCatDataSimple?
I think a selector is easier (less code), but if you really want to use triggers, maybe something like this?
namespace WpfApplication65
{
public abstract class NotifyBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void NotifyPropertyChanged(string property)
{
if(PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(property));
}
}
}
public abstract class DataBase : NotifyBase
{
bool m_showDetailed;
public bool ShowDetailed
{
get { return m_showDetailed; }
set
{
m_showDetailed = value;
NotifyPropertyChanged("ShowDetailed");
}
}
}
public class DogData : DataBase { }
public class CatData : DataBase { }
public partial class MainWindow : Window
{
public List<DataBase> Items { get; private set; }
public MainWindow()
{
Items = new List<DataBase>() { new DogData(), new CatData(), new DogData() };
DataContext = this;
InitializeComponent();
}
}
}
<Window x:Class="WpfApplication65.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:l="clr-namespace:WpfApplication65"
Title="MainWindow"
WindowStartupLocation="CenterScreen">
<Window.Resources>
<DataTemplate DataType="{x:Type l:CatData}">
<DataTemplate.Resources>
<DataTemplate x:Key="basic">
<TextBlock Text="This is the basic cat view" />
</DataTemplate>
<DataTemplate x:Key="detailed">
<TextBlock Text="This is the detailed cat view" />
</DataTemplate>
</DataTemplate.Resources>
<Border BorderThickness="1"
BorderBrush="Red"
Margin="2"
Padding="2">
<StackPanel>
<ContentPresenter Name="PART_Presenter"
ContentTemplate="{StaticResource basic}" />
<CheckBox Content="Show Details"
IsChecked="{Binding ShowDetailed}" />
</StackPanel>
</Border>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding ShowDetailed}"
Value="True">
<Setter TargetName="PART_Presenter"
Property="ContentTemplate"
Value="{StaticResource detailed}" />
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
<DataTemplate DataType="{x:Type l:DogData}">
<DataTemplate.Resources>
<DataTemplate x:Key="basic">
<TextBlock Text="This is the basic dog view" />
</DataTemplate>
<DataTemplate x:Key="detailed">
<TextBlock Text="This is the detailed dog view" />
</DataTemplate>
</DataTemplate.Resources>
<Border BorderThickness="1"
BorderBrush="Blue"
Margin="2"
Padding="2">
<StackPanel>
<ContentPresenter Name="PART_Presenter"
ContentTemplate="{StaticResource basic}" />
<CheckBox Content="Show Details"
IsChecked="{Binding ShowDetailed}" />
</StackPanel>
</Border>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding ShowDetailed}"
Value="True">
<Setter TargetName="PART_Presenter"
Property="ContentTemplate"
Value="{StaticResource detailed}" />
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</Window.Resources>
<ListBox ItemsSource="{Binding Items}" />
</Window>
i have been successfully adding an item to list in a MVVM, and now my problem is maintaining the list in the view model. Every time i navigate to a page or go back to a page and return to that listview, the list resets. how will i able to achieve that? i am currently using the prism to build the MVVM.
The ViewModel:
public ObservableCollection<CartData> _cartData;
public ObservableCollection<CartData> CartData
{
get {
return _cartData;
}
set {
SetProperty(ref _cartData, value);
}
}
private DelegateCommand _addItemCommand;
public ICommand AddItemCommand
{
get
{
if (_addItemCommand == null)
{
_addItemCommand = new DelegateCommand(AddToCart);
}
return _addItemCommand;
}
}
public void AddToCart() {
CartData.Add(new CartData { Cakename = "Black Forest", Cakeprice = 104 });
}
View:
.....
<Page.DataContext>
<vm:CartingDataSource/>
</Page.DataContext>
....
<ListView
x:Name="itemListView"
AutomationProperties.AutomationId="ItemsListView"
AutomationProperties.Name="Items"
TabIndex="1"
Margin="-10,130,0,264"
Padding="120,0,0,60"
ItemsSource="{Binding cartData}"
IsSwipeEnabled="False" Grid.RowSpan="2" ItemClick="itemListView_ItemClick" SelectionChanged="itemListView_SelectionChanged_1" IsItemClickEnabled="True">
<ListView.ItemTemplate>
<DataTemplate>
<Grid Margin="6">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Border Background="{ThemeResource ListViewItemPlaceholderBackgroundThemeBrush}" Width="60" Height="60">
<Image Stretch="UniformToFill" AutomationProperties.Name="{Binding Title}" Source="Assets/wewewew.jpg"/>
</Border>
<StackPanel Grid.Column="1" Margin="10,0,0,0">
<TextBlock Text="{Binding Cakename}" Style="{StaticResource TitleTextBlockStyle}" TextWrapping="NoWrap" MaxHeight="40"/>
<TextBlock Text="{Binding Cakeprice}" Style="{StaticResource CaptionTextBlockStyle}" TextWrapping="NoWrap"/>
</StackPanel>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
<ListView.ItemContainerStyle>
<Style TargetType="FrameworkElement">
<Setter Property="Margin" Value="0,0,0,10"/>
</Style>
</ListView.ItemContainerStyle>
</ListView>
Here if your ViewModel is CartingDataSource in that case it is being instatiated on every page load. Now if that is the case, then you are creating a new instance of your collection in your constructor as below:
public CartingDataSource() {
CartData = new ObservableCollection<CartData>();
}
As a result of which it re-initializes your collection.
You need to remove the initialization from your constructor and do something like this:
public ObservableCollection<CartData> _cartData;
public ObservableCollection<CartData> cartData
{
get {
if(_cartData == null)
{
_cartData = new ObservableCollection<CartData>();
}
return _cartData;
}
set {
_cartData = value;
}
}