I have a problem similar to How to get an ItemsSource to refresh its bind?
But I did used the INotifyPropertyChange interface and still having the issue. Here is the XAML:
<UserControl x:Class="Sample.Module.Pages.View.ModifyDataTypeView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:Controls="http://metro.mahapps.com/winfx/xaml/controls"
xmlns:Dialog="clr-namespace:MahApps.Metro.Controls.Dialogs;assembly=MahApps.Metro"
xmlns:local="clr-namespace:Sample.Module.Pages.ViewModel"
mc:Ignorable="d"
d:DesignHeight="500" d:DesignWidth="350">
<UserControl.DataContext>
<local:DataTypeViewModel/>
</UserControl.DataContext>
<Canvas>
<ComboBox Canvas.Left="0" Canvas.Top="0"
Margin="13,22,0,0"
Style="{DynamicResource VirtualisedMetroComboBox}"
Controls:TextBoxHelper.Watermark="Autocompletion"
DisplayMemberPath="DataTypeName"
IsEditable="True"
ItemsSource="{Binding DataTypes}"
MaxDropDownHeight="125"
SelectedItem="{Binding Path=SelectedDataType, Mode=TwoWay}" Height="25"/>
</Canvas>
Here is the ViewModel:
public class DataTypeViewModel : DomainObject
{
private ObservableCollection<DataTypeRepository> dataTypes;
private DataTypeRepository selectedDataType;
private DataTypeModel dataTypeModel;
public DataTypeViewModel()
{
dataTypeModel = new DataTypeModel();
selectedDataType = new DataTypeRepository();
this.dataTypes = dataTypeModel.GetAllDataTypes();
InsertDataTypeCommand = new DelegateCommand(OnInsertDataType);
}
public ObservableCollection<DataTypeRepository> DataTypes
{
get { return dataTypes; }
set
{
if (!EqualityComparer<ObservableCollection<DataTypeRepository>>.Default.Equals(dataTypes,value))
{
dataTypes = value;
RaisePropertyChanged("DataTypes");
}
}
}
public DataTypeRepository SelectedDataType
{
get
{
return selectedDataType;
}
set
{
if (!EqualityComparer<DataTypeRepository>.Default.Equals(selectedDataType, value))
{
selectedDataType = value;
InsertDataTypeCommand.RaiseCanExecuteChanged();
RaisePropertyChanged("SelectedDataType");
}
}
}
public DelegateCommand InsertDataTypeCommand { get; private set; }
private void OnInsertDataType()
{
DataBaseOperationStatusMessage = dataTypeModel.InsertDataType(selectedDataType);
DatabaseOperationComplete = true;
DataTypes = dataTypeModel.GetAllDataTypes();
}
}
Please note the InsertDataTypeCommand command is used in another another tab of the app where the "DataType" is added. After adding a DataType user clicks to the Modify Tab to see the new DataType in the combobox list. But that is not happenning. If you restart the app and go to the Modify page you can see the new record.
The issue is the Combobox is not getting updated even when am using INotifyPropertyChange.
What am I missing here?
Try updating your binding to include the update trigger...
SelectedItem="{Binding Path=SelectedDataType, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
I would guess that the following line isn't causing the collection to be updated:
DataTypes = dataTypeModel.GetAllDataTypes();
From your code it looks like you go and insert the new item into your database or wherever, and are then running the method that loads all the items from the database. Instead of running that method it would be better, when you are inserting into the database, to create a corresponding DataTypeRepository item and add that to the collection.
this.DataTypes.Add(new DataTypeRepository { property1= "somedata"});
It just means if you're adding a new item onto what could be a huge list of items, you won't have to reload everything.
You should probably also update your itemsource to be 2 way binding (unless you're doing the readonly that Will suggested in the comments) and put the updatetrigger to property changed (by default its on focus lost i think)
Related
I have a ListView that I would like to populate in XAML. I'm using a custom DataTemplate to make each ListViewItem added contain a Label and a TextBlock.
The problem is I need to dynamically populate the text of the TextBlock of each ListViewItem with data from a settings property, and I don't know how to create this binding.
Right now I am populating the ListView with an XmlDataProvider, but I can't (or at least can't figure out how to) bind values to the xml data. (I'm not stuck using this method of data population, it's just what I was originally doing when I ran into this problem.)
Basically I need something as follows:
The user enters some data into a text box. That data is saved to user settings. When that happens, the corresponding TextBlock of the ListViewItem in the ListView is updated with the user setting data.
Normally I would bind a TextBlock's text to a user setting as follows:
Text="{Binding Source={x:Static properties:Settings.Default},Path=User_Data_1}"
But how do I do this when the text of the TextBlock is defined in the DataTemplate?
My DataTemplate and XmlDataProvider:
<DataTemplate x:Key="listViewTemplate">
<StackPanel Orientation="Vertical">
<Label x:Name="lblName" Content="{Binding XPath=name}"/>
<TextBlock x:Name="tbValue" Text="{Binding XPath=value}"/>
</StackPanel>
</DataTemplate>
<XmlDataProvider x:Key="PagesData" XPath="Pages">
<x:XData>
<Pages xmlns="">
<page id="page01">
<name>Text file:</name>
<value></value>
<source>Pages/Page_CreateFiles1.xaml</source>
</page>
<page id="page02">
<name>Xml file:</name>
<value></value>
<source>Pages/Page_CreateFiles2.xaml</source>
</page>
<page id="page03">
<name>Memory object database:</name>
<value></value>
<source>Pages/Page_CreateFiles3.xaml</source>
</page>
<page id="page04">
<name>Output database:</name>
<value></value>
<source>Pages/Page_CreateDB.xaml</source>
</page>
</Pages>
</x:XData>
</XmlDataProvider>
My ListView
<ListView x:Name="lvNavigation"
ItemTemplate="{DynamicResource listViewTemplate}"
ItemsSource="{Binding Source={StaticResource PagesData}, XPath=page}"/>
Create a view model with a collection of items
public class Item
{
public string Name { get; set; }
public string Value { get; set; }
}
public class ViewModel
{
public ObservableCollection<Item> Items { get; } = new ObservableCollection<Item>();
}
and set the MainWindow's DataContext to an instance of the view model class
public MainWindow()
{
InitializeComponent();
var vm = new ViewModel();
DataContext = vm;
vm.Items.Add(new Item { Name = "Name 1", Value = "Value 1" });
vm.Items.Add(new Item { Name = "Name 2", Value = "Value 2" });
}
Bind to it like this:
<ItemsControl ItemsSource="{Binding Items}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding Name}"/>
<TextBlock Text="{Binding Value}"/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
Clemmens answer is right, but I just wanted to put it out there that I basically did his approach with a slight modification. I used events to trigger the data change in the listview.
I think it is my own fault though, because I didn't explain my problem well enough. First of all, I wanted to do everything from xaml and I don't think that was possible. Second, I failed to mention that I was using pages in a frame, where the data was coming from the pages and the listview was in my main window that contained the frame. So that's why I ended up using events to communicate between the page and the main window.
So in my main window I've defined my observable collection:
ObservableCollection<NavItem> NavItems = new ObservableCollection<NavItem>();
public MainWindow()
{
InitializeComponent();
NavItems.Add(new NavItem { Name = "Text file:", Value = "", Source = "Pages/Page_CreateFiles.xaml" });
NavItems.Add(new NavItem { Name = "Xml file:", Value = "", Source = "Pages/Page_CreateFiles.xaml" });
NavItems.Add(new NavItem { Name = "Memory object db:", Value = "", Source = "Pages/Page_CreateFiles.xaml" });
NavItems.Add(new NavItem { Name = "Output database:", Value = "", Source = "Pages/Page_CreateDB.xaml" });
lvNavigation.ItemsSource = NavItems;
...
}
"NavItem" is a class that is subscribed to INotifyPropertyChanged. Posting that code will just be a lot, so check out how to do that here: INotifyPropertyChanged
Then in each page I set up an event that I call with the data to send:
public static event EventHandler<NavUpdateMessage> UpdateMessage;
private void OnUpdateMessage(int id, string message)
{
NavUpdateMessage navUpdateMessage = new NavUpdateMessage();
navUpdateMessage.Id = id;
navUpdateMessage.Message = message;
var e = UpdateMessage;
if (e != null)
e(this, navUpdateMessage);
}
With the main window subscribed to that event:
public MainWindow()
{
...
Pages.Page_CreateFiles.UpdateMessage += Pages_UpdateMessage;
Pages.Page_CreateDB.UpdateMessage += Pages_UpdateMessage;
}
private void Pages_UpdateMessage(object sender, NavUpdateMessage e)
{
Dispatcher.BeginInvoke(new Action(() =>
{
NavItems[e.Id].Value = e.Message;
}));
}
I'm sure there's a better, more simple approach to this, but this is what I could figure out. And even though I'm sure no one will see this because this question definitely did not get any traction, please feel free to suggest a better solution so at least I can learn.
In my view i'm adding dynamically custom TabItems (TextseiteTabItem). With the property DataContext i gave each TabItem a Model to work with (fill in values). Now i added a close-command to the custom TabItems but it wont work. I cant manage to send the close-command to the viewmodel. Above is my attempt..
My custom TabItem:
<sdk:TabItem x:Class="PortfolioCreator.TextseiteTabItem"
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"
mc:Ignorable="d"
xmlns:navigation="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Navigation"
xmlns:sdk="http://schemas.microsoft.com/winfx/2006/xaml/presentation/sdk"
xmlns:toolkit="http://schemas.microsoft.com/winfx/2006/xaml/presentation/toolkit">
<sdk:TabItem.Header>
<StackPanel Orientation="Horizontal">
<sdk:Label Content="{Binding Seitennummer, StringFormat='Seite {0}', Mode=TwoWay}"/>
<Button Content="X"
Command="{Binding CloseTabCommand, Mode=TwoWay}"
DataContext="{Binding ElementName=TemplateTabControl}"
CommandParameter="{Binding SelectedItem, ElementName=TemplateTabControl}" />
</StackPanel>
</sdk:TabItem.Header>
<sdk:TabItem.Content>
<Grid x:Name="LayoutRoot">
...
</Grid>
</sdk:TabItem.Content>
</sdk:TabItem>
In my View:
...
<sdk:TabControl toolkit:DockPanel.Dock="Bottom" ItemsSource="{Binding Tabs}" x:Name="TemplateTabControl"/>
...
In my ViewModel:
public class PortfolioViewModel : ViewModelBase
{
public ObservableCollection<TabItem> Tabs { get; set; }
public RelayCommand<TabItem> CloseTabCommand
{
get;
private set;
}
public PortfolioViewModel()
{
CloseTabCommand = new RelayCommand<TabItem>(tab =>
{
//never reached
},
tab =>
{
//never reached
});
Tabs = new ObservableCollection<TabItem>();
AddTextseite();
AddTextseite();
}
void AddTextseite()
{
TabItem item = new TextseiteTabItem();
item.DataContext = new TextSeiteModel();
Tabs.Add(item);
}
}
First, your CloseTabCommand does nothing in your current code snippet: //never reached. The execute handler should read something like tab.Visibility = Visibility.Collapsed or myTabControl.Items.Remove(myTabItem).
Second, as #Rafal pointed out, using UI elements in the ViewModel is not the correct way to implement MVVM. If you want closable tab items, the correct way would be to derive a generic CloseableTabItem control or write a ClosableTabItemBehavior on the UI layer with a settable ICommand CloseCommand that can be bound to the corresponding ICommand instance on the ViewModel. Admittedly this approach might be too elaborate for your project though.
You are attempting to use MVVM but the strange thing I see is collection of ui elements (Tabs) in your view model. The correct way would be to create ViewModel that describes Tab item and move the command there. Then it will bind. To remove tab from Tabs you should expose event in your Tab view model and attach to it form PortfolioViewModel.
Of course my change will cause that your TextseiteTabItem will not show in TablControl. But it can be easily fixed with TabControl.ItemTemplate and TabControl.ContentTemplate.
here you find a demo application with closeable tabs for wpf, maybe it works for your silverlight version also.
This is my workaround for this problem. I admit it is not a good solution and breaks the mvvm pattern but as #herzmeister says other approaches are too elaborate for my project right now. (But it won't be the final solution ;-) )
TabItemViewModel:
public delegate void CloseTabItemHandler();
public class TextseiteTabItemViewModel : ViewModelBase
{
public event CloseTabItemHandler CloseTabItem;
public RelayCommand CloseTabCommand {get; set;}
public TextseiteTabItemViewModel()
{
CloseTabCommand = new RelayCommand(() =>
{
if (CloseTabItem == null) return;
CloseTabItem();
});
}
}
TabItemView:
<sdk:TabItem x:Class="PortfolioCreator.TextseiteTabItemView"
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"
mc:Ignorable="d"
xmlns:navigation="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Navigation"
xmlns:sdk="http://schemas.microsoft.com/winfx/2006/xaml/presentation/sdk"
xmlns:toolkit="http://schemas.microsoft.com/winfx/2006/xaml/presentation/toolkit">
<sdk:TabItem.Header>
<StackPanel Orientation="Horizontal">
<Button Content="X" Command="{Binding CloseTabCommand, Mode=TwoWay}" />
</StackPanel>
</sdk:TabItem.Header>
<sdk:TabItem.Content>
<Grid x:Name="LayoutRoot">
...
</Grid>
</sdk:TabItem.Content>
</sdk:TabItem>
Parent ViewModel:
public class PortfolioViewModel : ViewModelBase
{
public ObservableCollection<TabItem> Tabs { get; set; }
public PortfolioViewModel()
{
Tabs = new ObservableCollection<TabItem>();
AddTextseite();
AddTextseite();
}
void AddTextseite()
{
var viewmodel = new TextseiteTabItemViewModel();
TabItem item = new TextseiteTabItemView();
item.DataContext = viewmodel;
viewmodel.CloseTabItem += new CloseTabItemHandler(() =>
{
Tabs.Remove(item);
});
Tabs.Add(item);
}
}
I want to use MVVM in my WPF application. I currently have a Model and a view which has DataGrid and some other controls. I created a ViewModel based on my model and don't know if I did it correctly. The view is just a simple dialog box. I want to fill the DataGrid view.
How can I tell the DataGrid to bind with the ViewModel?
I would like to bind properties(inside viewmodel like ID and Date) to the datagrid.
SO like if there is two objects inside the list I would like to see two rows in datagrid with the specific ID's and Date's.
Im setting the datacontext inside the class instead xaml.
Here is the code so far:
public class ViewModel : INotifyPropertyChanged
{
private string _id;
private DateTime _date;
private ObservableCollection<Object> _list;
public string Id
{
get { return _id; }
set
{
_id = value;
PropertChanged("Id");
}
}
public DateTime Date
{
get { return _date; }
set
{
_date = value;
PropertChanged("Date");
}
}
public ObservableCollection<Object> list
{
get { return _list; }
set
{
_list = value;
PropertChanged("list");
}
}
public LicenseViewModel()
{
list = GetList();
}
public event PropertyChangedEventHandler PropertyChanged;
public void PropertChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
And the XAML:
<Window x:Class="Import"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:Controls="clr-namespace:Microsoft.Windows.Controls;assembly=WPFToolkit"
mc:Ignorable="d"
ResizeMode="CanResizeWithGrip"
x:Name="ImportLicense"
d:DesignHeight="493" d:DesignWidth="559"
Title="Import Licenses" SizeToContent="WidthAndHeight">
<Grid Width="538">
<DataGrid x:Name="Imported" VerticalAlignment="Top" AutoGenerateColumns="False" CanUserResizeColumns="True">
<DataGrid.Columns>
<DataGridTextColumn Header="Entitlement ID" Binding="{Binding Path=ID}"/>
<DataGridTextColumn Header="Date Sold" Binding="{Binding Path=Date}"/>
</DataGrid.Columns>
</DataGrid>
</Grid>
</Window>
You need to set the data context for the DataGrid to the instance of your view model. You can do this by simply setting the DataContext of your View or the DataGrid to your instance of your view model in the constructor of the view class. This is a quick and dirty way of doing this.
If you want to be more sophisticated you can create a DepenencyProperty on your view class like this:
public static DependencyProperty ViewModelProperty =
DependencyProperty.Register("ViewModel",
typeof(ItemViewModel),
typeof(ViewClassHere));
public ItemViewModel ViewModel
{
get { return (ItemViewModel)base.GetValue(ItemViewModel); }
set { base.SetValue(ItemViewModel, value); }
}
Then you'd bind to that property in any one of many ways but one way would be like:
<DataGrid ItemsSource="{Binding ElementName=windowName, Path=viewName.list}">
There are a bunch of ways to do this, these are just two possible ways to do this.
the common way to display data with a datagrid is to set the itemssource
<DataGrid ItemsSource="{Binding MyCollection}"/>
your viewmodel defines 2properties and one collection, but in your xaml you bind your properties to the datagrid columns and don't set any itemssource.
its not clear to me what you would like to see in your datagrid, but your 2 properties ID and DateTime are not part of any collection, so why you want this to display in your datagrid?
please edit your question and give some information of what you wanna see in your datagrid.
Try like this:
<window.Resources>
<ViewModel x:Key="ViewModel"></ViewModel >
</window.Resources>
<Grid x:Name="ValueDetail" DataContext="{StaticResource ViewModel}">
<DataGrid ItemsSource="{Binding MyCollection}"/>
</Grid>
I have a WPF app which I am using Blend to style.
One of my view models is of the type:
public Dictionary<DateTime, ObservableCollection<MyViewModel>> TimesAndEvents
But when I try to create some sample data in Expression Blend it simply doesnt create the XAML for this property.
Can you create a data type like this in XAML? The non-design time support is killing my productivity.
Regarding your last question: unfortunately, you cannot easily instantiate dictionaries in WPF. I believe this answer explains that part well. The book, WPF 4.5 Unleashed provides a good summary of what the linked answer states:
A common workaround for this limitation (not being able to instantiate
a dictionary in WPF's version of XAML) is to derive a non-generic
class from a generic one simply so it can be referenced from XAML...
But even then, instantiating that dictionary in xaml is again, in my opinion, a painful process. Additionally, Blend does not know how to create sample data of that type.
Regarding the implicit question of how to get design time support: there are a few ways to achieve design time data in WPF, but my preferred method at this point in time for complex scenarios is to create a custom DataSourceProvider. To give credit where it is due: I got the idea from this article (which is even older than this question).
The DataSourceProvider Solution
Create a class that implements DataSourceProvider and returns a sample of your data context. Passing the instantiated MainWindowViewModel to the OnQueryFinished method is what makes the magic happen (I suggest reading about it to understand how it works).
internal class SampleMainWindowViewModelDataProvider : DataSourceProvider
{
private MainWindowViewModel GenerateSampleData()
{
var myViewModel1 = new MyViewModel { EventName = "SampleName1" };
var myViewModel2 = new MyViewModel { EventName = "SampleName2" };
var myViewModelCollection1 = new ObservableCollection<MyViewModel> { myViewModel1, myViewModel2 };
var timeToMyViewModelDictionary = new Dictionary<DateTime, ObservableCollection<MyViewModel>>
{
{ DateTime.Now, myViewModelCollection1 }
};
var viewModel = new MainWindowViewModel()
{
TimesAndEvents = timeToMyViewModelDictionary
};
return viewModel;
}
protected sealed override void BeginQuery()
{
OnQueryFinished(GenerateSampleData());
}
}
All that you have to do now is add your data provider as a sample data context in your view:
<Window x:Class="SampleDataInBlend.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:SampleDataInBlend"
mc:Ignorable="d"
Title="MainWindow" Height="200" Width="300">
<d:Window.DataContext>
<local:SampleMainWindowViewModelDataProvider/>
</d:Window.DataContext>
<Grid>
<ListBox ItemsSource="{Binding TimesAndEvents}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Key}"/>
<ListBox ItemsSource="{Binding Value}">
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type local:MyViewModel}">
<TextBlock Text="{Binding EventName}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</Window>
Note: the 'd' in <d:Window.DataContext> is important as it tells Blend and the compiler that that specific element is for design time and it should be ignored when the file is compiled.
After doing that, my design view now looks like the following:
Setting up the problem
I started with 5 classes (2 were generated from the WPF project template, which I recommend using for this):
MyViewModel.cs
MainWindowViewModel.cs
MainWindow.xaml
App.xaml
MyViewModel.cs
public class MyViewModel
{
public string EventName { get; set; }
}
MainWindowViewModel.cs
public class MainWindowViewModel
{
public IDictionary<DateTime, ObservableCollection<MyViewModel>> TimesAndEvents { get; set; } = new Dictionary<DateTime, ObservableCollection<MyViewModel>>();
public void Initialize()
{
//Does some service call to set the TimesAndEvents property
}
}
MainWindow.cs
I took the generated MainWindow class and changed it. Basically, now it asks for a MainWindowViewModel and sets it as its DataContext.
public partial class MainWindow : Window
{
public MainWindow(MainWindowViewModel viewModel)
{
DataContext = viewModel;
InitializeComponent();
}
}
MainWindow.xaml
Please note the lack of the design data context from the Solution.
<Window x:Class="SampleDataInBlend.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:SampleDataInBlend"
mc:Ignorable="d"
Title="MainWindow" Height="200" Width="300">
<Grid>
<ListBox ItemsSource="{Binding TimesAndEvents}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Key}"/>
<ListBox ItemsSource="{Binding Value}">
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type local:MyViewModel}">
<TextBlock Text="{Binding EventName}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</Window>
App.cs
First off, remove StartupUri="MainWindow.xaml" from the xaml side as we'll be launching MainWindow from the code behind.
public partial class App : Application
{
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
var viewModel = new MainWindowViewModel();
// MainWindowViewModel needs to have its dictionary filled before its
// bound to as the IDictionary implementation we are using does not do
// change notification. That is why were are calling Initialize before
// passing in the ViewModel.
viewModel.Initialize();
var view = new MainWindow(viewModel);
view.Show();
}
}
Build and run
Now, if everything was done correctly and you fleshed out MainWindowViewModel's Initialize method (I will include my implementation at the bottom), you should see a screen like the one below when you build and run your WPF app:
What was the problem again?
The problem was that nothing was showing in the design view.
My Initialize() method
public void Initialize()
{
TimesAndEvents = PretendImAServiceThatGetsDataForMainWindowViewModel();
}
private IDictionary<DateTime, ObservableCollection<MyViewModel>> PretendImAServiceThatGetsDataForMainWindowViewModel()
{
var myViewModel1 = new MyViewModel { EventName = "I'm real" };
var myViewModel2 = new MyViewModel { EventName = "I'm real" };
var myViewModelCollection1 = new ObservableCollection<MyViewModel> { myViewModel1, myViewModel2 };
var timeToMyViewModelDictionary = new Dictionary<DateTime, ObservableCollection<MyViewModel>>
{
{ DateTime.Now, myViewModelCollection1 }
};
return timeToMyViewModelDictionary;
}
Any more I've gone the route of creating a Design Time Instance of my Viewmodel in my Locator that I reference as #ChrisW suggested above:
d:DataContext="{Binding Source={StaticResource Locator}, Path=DesignTimeVM}"
So I can have some hard-coded values to populate my lists, comboboxes, etc. Makes styling everything that much easier.
I use MVVM Light and so in my ViewModel's constructor I use a pattern like this:
if(IsInDesignMode)
{
ListUsers = new List<User>();
.
.
.
}
The code will only execute at Design Time, and you will have your Xaml UI bound to actual data.
Since Xaml 2009 support generic types, is possible write a loose xaml(can not be compiled in wpf project) like this to represent a dictionary.
Data.xaml
<gnrc:Dictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
xmlns:gnrc="clr-namespace:System.Collections.Generic;assembly=mscorlib"
xmlns:om="clr-namespace:System.Collections.ObjectModel;assembly=System"
x:TypeArguments="sys:DateTime,om:ObservableCollection(x:String)">
<om:ObservableCollection x:TypeArguments="x:String">
<x:Key>
<sys:DateTime>2017/12/31</sys:DateTime>
</x:Key>
<x:String>The last day of the year.</x:String>
<x:String>Party with friends.</x:String>
</om:ObservableCollection>
<om:ObservableCollection x:TypeArguments="x:String">
<x:Key>
<sys:DateTime>2018/1/1</sys:DateTime>
</x:Key>
<x:String>Happy new year.</x:String>
<x:String>Too much booze.</x:String>
</om:ObservableCollection>
<om:ObservableCollection x:TypeArguments="x:String">
<x:Key>
<sys:DateTime>2018/1/10</sys:DateTime>
</x:Key>
<x:String>Just another year.</x:String>
<x:String>Not much difference.</x:String>
</om:ObservableCollection>
</gnrc:Dictionary>
But it is not support by designers like Blend or Visual Studio. If you put it into a xaml that associated with a designer, you will get dozens of errors. To solve this, we need a markup extension to provide value from Data.xaml by using XamlReader.Load method.
InstanceFromLooseXamlExtension.cs
public class InstanceFromLooseXamlExtension : MarkupExtension
{
public Uri Source { get; set; }
public override object ProvideValue(IServiceProvider serviceProvider)
{
if (Source == null)
{
throw new ArgumentNullException(nameof(Source));
}
Uri source;
if (Source.IsAbsoluteUri)
{
source = Source;
}
else
{
var iuc = serviceProvider?.GetService(typeof(IUriContext)) as IUriContext;
if (iuc == null)
{
throw new ArgumentException("Bad service contexts.", nameof(serviceProvider));
}
source = new Uri(iuc.BaseUri, Source);
}
WebResponse response;
if (source.IsFile)
{
response = WebRequest.Create(source.GetLeftPart(UriPartial.Path)).GetResponse();
}
else if(string.Compare(source.Scheme, PackUriHelper.UriSchemePack, StringComparison.Ordinal) == 0)
{
var iwrc = new PackWebRequestFactory() as IWebRequestCreate;
response = iwrc.Create(source).GetResponse();
}
else
{
throw new ArgumentException("Unsupported Source.", nameof(Source));
}
object result;
try
{
result = XamlReader.Load(response.GetResponseStream());
}
finally
{
response.Close();
}
return result;
}
}
This markup extension has a Uri type Source property to let user specify which xaml file to load. Then finally, use the markup extension like this.
MainWindow.xaml
<Window x:Class="WpfApp.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:WpfApp"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<ListBox ItemsSource="{local:InstanceFromLooseXaml Source=/Data.xaml}">
<ListBox.ItemTemplate>
<DataTemplate>
<Expander Header="{Binding Key}">
<ListBox ItemsSource="{Binding Value}"/>
</Expander>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Window>
In this case, I place Data.xaml in application folder, so 'Source=/Data.xaml' will be OK. Every time the designer reloaded(a rebuild will ensure it), the contents in loose xaml will be applied. The result should look like
The loose xaml can contain almost everything, like a ResourceDictionary or something with UiElements. But both Blend or Visual Studio will not check it correctly for you. In the end, hope this is enough for an answer.
I have a datagrid which contains bid and ask - prices for currency-symbols. The data is updated every seconds. I update the data by creating a new collection of my viewmodel-entities and bind this collection to the datagrid every second.
The problem is:
Because my datagrid contains a template-column with a button "buy", this button is also recreated every second! This means, that when the user hovers the button, the hover-styles blinks, as the button is recreated every second. Additionally sometimes the click-event is not fired correctly, if the button is recreated while the user has his mouse-left-button pressed.
Any suggestions, how to solve real-time-update a datagrid with button-columns?
If I understand correctly, you have a collection of items and you have a couple fields (bid/ask in particular), all of which will be updated every second. It sounds like what may be happening is that in the process of changing the ItemsSource of your data grid, you're losing some important state that is causing problems for the event handlers on your buttons.
Even if you update all of the items, the important distinction to make is that you should update the items, and not completely clear out the collection that is currently bound to your datagrid. Changing the ItemsSource to a new one will cause the data grid to have to do a lot more work than if you simply update the contents of the existing collection. If you're using an ObservableCollection, this may mean making your viewmodel items mutable so that you can simply update bid/ask. If your viewmodel items are mutable and implement INotifyPropertyChanged, the bid/ask updates will be reflected in the datagrid or in any other bindings to those properties of the objects. The neat thing about doing it this way is that the same objects are staying bound to the same containers in the ItemsControl, so during each update, absolutely nothing is happening to your buttons. Now, if your viewmodel objects that contain bid/ask are immutable, you should still be able to pull this off. Every second, you simply iterate through your collection of items and use SetItem to replace each existing item with a new one. The important thing to remember in this latter case is that every second, the datagrid is still getting notified that there has been a change in the ObservableCollection, and because of this, the bindings on each row are going to cause the DataContext of the row/cells/button to update.
Here's a quick example of how I might go about this problem. I'm going to assume use of the datagrid in .NET 4.0 (if you're using toolkit though with 3.5, this should be the same). I'm going to take the first approach, where my CurrencyPair object is mutable.
First, some simple viewmodel code with a self contained timer to update a few currency pair bid/asks each second:
public class CurrencyPairsViewModel
{
private readonly Dispatcher _dispatcher = Dispatcher.CurrentDispatcher;
private readonly ObservableCollection<string> _orders = new ObservableCollection<string>();
private readonly ObservableCollection<CurrencyPair> _pairs = new ObservableCollection<CurrencyPair>();
private readonly Random _rand = new Random();
private readonly System.Timers.Timer _timer = new System.Timers.Timer(1000);
private readonly Action _update;
public CurrencyPairsViewModel()
{
this._timer.Elapsed += OnIntervalElapsed;
this._update = new Action(this.Update);
this._pairs.Add(new CurrencyPair("USD/GBP"));
this._pairs.Add(new CurrencyPair("AUD/USD"));
this._pairs.Add(new CurrencyPair("WOW/CAD"));
this._timer.Start();
}
public ObservableCollection<string> Orders { get { return this._orders; } }
public ObservableCollection<CurrencyPair> Pairs { get { return this._pairs; } }
public void Buy(CurrencyPair pair)
{
this._orders.Add(string.Format("Buy {0} at {1}", pair.Name, pair.Ask));
}
private void OnIntervalElapsed(object sender, System.Timers.ElapsedEventArgs e)
{
this._dispatcher.Invoke(this._update);
}
private void Update()
{
foreach (var pair in this._pairs)
{
pair.Bid = this._rand.NextDouble();
pair.Ask = pair.Bid + 0.01;
}
this._timer.Start();
}
}
public class CurrencyPair : INotifyPropertyChanged
{
private readonly string _name;
private double _ask;
private double _bid;
public CurrencyPair(string name)
{
this._name = name;
}
public event PropertyChangedEventHandler PropertyChanged;
public double Ask
{
get { return this._ask; }
set
{
this._ask = value;
this.OnPropertyChanged("Ask");
}
}
public double Bid
{
get { return this._bid; }
set
{
this._bid = value;
this.OnPropertyChanged("Bid");
}
}
public string Name { get { return this._name; } }
protected void OnPropertyChanged(string name)
{
if (null != this.PropertyChanged)
{
this.PropertyChanged(this, new PropertyChangedEventArgs(name));
}
}
}
Second, the view, which in this example is just my MainWindow.
<Window x:Class="WpfApplication1.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="*"/>
<RowDefinition Height="4"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<DataGrid Grid.Row="0"
ItemsSource="{Binding Pairs}"
AutoGenerateColumns="False">
<DataGrid.Columns>
<DataGridTextColumn Header="Name" Binding="{Binding Name}" Width="*"/>
<DataGridTextColumn Header="Bid" Binding="{Binding Bid}" Width="*"/>
<DataGridTextColumn Header="Ask" Binding="{Binding Ask}" Width="*"/>
<DataGridTemplateColumn Header="Buy">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Button Content="BUY"
Click="OnBuyClicked"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
<GridSplitter Grid.Row="1" Height="4" HorizontalAlignment="Stretch" VerticalAlignment="Center"/>
<ListBox Grid.Row="2"
ItemsSource="{Binding Orders}"/>
</Grid>
</Window>
And finally, I have a bit of code behind this XAML to handle the BUY button clicks and initialize a viewmodel right in the view (note that this, and other practices outside of how to update the bid/ask on the collection of items may not be the best way to go about things, depending on how your application is going to grow).
public partial class MainWindow : Window
{
private readonly CurrencyPairsViewModel _model = new CurrencyPairsViewModel();
public MainWindow()
{
InitializeComponent();
this.DataContext = this._model;
}
private void OnBuyClicked(object sender, RoutedEventArgs e)
{
var pair = (CurrencyPair)((Button)sender).DataContext;
this._model.Buy(pair);
}
}
Hope the example is helpful!
Have you looked into the ObservableCollection?
Represents a dynamic data collection that provides notifications when items get added, removed, or when the whole list is refreshed.
This should only refresh those items that are changed, rather than the whole grid.