I have run into an issue with combo boxes in WPF where they seem to hang onto the first DataContext they were opened with. When I change the DataContext on my ComboBox, a child PopupRoot object still references the old DataContext.
At first I assumed we were doing something wrong but I was having trouble working out what that might be so I tried to simplify. I have managed to recreate the behavior I am seeing in our application in a very simple form so it seems more like a bug in the WPF ComboBox implementation. That sounds a little controversial so I thought I'd turn to stackoverflow for help.
The core code for the sample is below:
<Window x:Class="ComboBoxTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="150" Width="525">
<DockPanel>
<Button Click="ReloadModel" Width="137" Height="40">Reload Model</Button>
<ComboBox Name="ComboBox"
ItemsSource="{Binding AvailableOptions}"
SelectedItem="{Binding SelectedOption}"
Width="235" Height="43">
</ComboBox>
</DockPanel>
</Window>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
var newModel = new ViewModel();
ComboBox.DataContext = newModel;
}
private void ReloadModel(object sender, RoutedEventArgs e)
{
var newModel = new ViewModel();
ComboBox.DataContext = newModel;
}
}
public class ViewModel : INotifyPropertyChanged
{
public ViewModel()
: this(new[] { "Option 1", "Option 2", "Option 3" })
{ }
public ViewModel(IEnumerable<string> options)
{
_selectedOption = options.First();
_availableOptions = new ObservableCollection<string>(options);
}
protected void RaisePropertyChanged(string propertyName)
{
var propertyChangedHandler = PropertyChanged;
if (propertyChangedHandler != null)
{
propertyChangedHandler(this, new PropertyChangedEventArgs(propertyName));
}
}
public event PropertyChangedEventHandler PropertyChanged;
private readonly ObservableCollection<string> _availableOptions;
public ObservableCollection<string> AvailableOptions
{
get
{
return _availableOptions;
}
}
private string _selectedOption;
public string SelectedOption
{
get { return _selectedOption; }
set
{
if (_selectedOption == value)
{
return;
}
_selectedOption = value;
RaisePropertyChanged("SelectedOption");
}
}
}
Steps to reproduce:
1) Run Application
2) Open Combobox (so that it renders the drop down options)
3) Click "Reload Model" button
At this point there will be be two ViewModel objects, the older, unexpected instance is rooted like:
ViewModel->PopupRoot->Popup->ComboBox->MainWindow->App
Is this a bug or am I doing it wrong?
Eamon
Joe's comment brought my attention back to this old question which I have solved for my own use. In the end I wrote a Behavior that I could attach to a combobox that dealt with the memory leak.
I've posted the code here: https://github.com/EamonHetherton/Demos/blob/master/StackOverflow/18096050/StopComboBoxMemoryLeakBehaviour.cs
caveat emptor: this solution relies on reflection and the fragility that could entail. It works for me, YMMV.
Recently I encountered several memory leak problems which were related to Popup / ContextMenu / ComboBox binding with DataContext.
I found out that essentially the problem for Popup / ComboBox was that the "_popupRoot"'s DataContext was not released after the DataContext of its parents were set to null.
For ContextMenu, if it's used with some kind of ItemsSource binding generated controls, then WPF will cache the Contextmenu, so its DataContext will not be released unless the user right click to pop up the ContextMenu somewhere again.
I managed to create 3 derived classes to replace the WPF controls where DataContext binding was used. I will paste them here, hopefully, they may be useful to someone else.
public class ComboBoxFixMem : ComboBox
{
public ComboBoxFixMem()
{
this.DataContextChanged += ComboBox_DataContextChanged;
}
private void ComboBox_DataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
{
if (this.DataContext != null)
return;
FrameworkElement fe = this.GetTemplateChild("PART_Popup") as FrameworkElement;
if (null != fe)
fe.DataContext = null;
PopupFixMem.ClearPopupDataContext(fe as Popup);
}
}
public class ContextMenuFixMem : ContextMenu
{
protected override void OnClosed(RoutedEventArgs e)
{
base.OnClosed(e);
FrameworkElement p = this.Parent as FrameworkElement;
if (null != p)
p.DataContext = null;
}
}
public class PopupFixMem : Popup
{
public PopupFixMem()
{
this.DataContextChanged += Popup_DataContextChanged;
}
private void Popup_DataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
{
if (this.DataContext != null)
return;
ClearPopupDataContext(this);
}
public static void ClearPopupDataContext(Popup popup)
{
if (null == popup)
return;
try
{
var fiPopupRoot = typeof(Popup).GetField("_popupRoot", BindingFlags.NonPublic | BindingFlags.Instance);
var popupRootWrapper = fiPopupRoot?.GetValue(popup);
if (null == popupRootWrapper)
return;
var valueFieldInfo = popupRootWrapper.GetType().GetProperty("Value", BindingFlags.NonPublic | BindingFlags.Instance);
var popupRoot = valueFieldInfo?.GetValue(popupRootWrapper, new object[0]) as FrameworkElement;
if (null != popupRoot)
popupRoot.DataContext = null;
}
catch (Exception) { }
}
}
Related
I am using a DevExpress ComboboxEdit object to get multiple selection from the user. My problem is that I am not sure what type of object will come back once a selection has been done.
I have read this one, and came up with the code below, but I am not sure what I am missing. (I also don't know exactly what a DependencyProperty is, but would like to avoid too many objects)
<Window x:Class = "Demo.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:Demo"
xmlns:dxe="http://schemas.devexpress.com/winfx/2008/xaml/editors"
xmlns:dxl="http://schemas.devexpress.com/winfx/2008/xaml/layoutcontrol"
xmlns:dx="http://schemas.devexpress.com/winfx/2008/xaml/core"
mc:Ignorable = "d"
Title = "MainWindow" Height = "350" Width = "525">
<StackPanel>
<dxe:ComboBoxEdit ItemsSource="{Binding Path=MyList}"
IsTextEditable="False"
EditValue="{Binding Path=MySelectedList, Mode=TwoWay}"
Name="abc">
<dxe:ComboBoxEdit.StyleSettings>
<dxe:CheckedComboBoxStyleSettings/>
</dxe:ComboBoxEdit.StyleSettings>
</dxe:ComboBoxEdit>
<Button Click="showSelected" Content="Show selected items" />
</StackPanel>
</Window>
MainWindow.xaml.cs
using System.Collections.Generic;
using System.Windows;
using System.Text;
namespace Demo
{
public partial class MainWindow : Window, INotifyPropertyChanged
{
private System.Collections.Generic.IList<string> _myList;
private System.Collections.Generic.IList<string> _mySelectedList; // This has probably the wrong type.
public event PropertyChangedEventHandler PropertyChanged;
protected void RaisePropertyChanged(string propertyName)
{
var handler = this.PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
public IList<string> MyList
{
get
{
return _myList;
}
set
{
_myList = value;
this.RaisePropertyChanged("MyList");
}
}
public IList<string> MySelectedList
{
get
{
return _mySelectedList;
}
set
{
_mySelectedList = value;
this.RaisePropertyChanged("MySelectedList");
}
}
private void showSelected(object sender, RoutedEventArgs e)
{
StringBuilder sb = new StringBuilder();
foreach(string s in this.MySelectedList)
{
sb.Append(s);
}
System.Windows.MessageBox.Show(sb.ToString());
// This MessageBox show show whatever is checked.
}
public MainWindow()
{
MySelectedList = new System.Collections.Generic.List<string>();
MyList = new System.Collections.Generic.List<string>();
MyList.Add("a");
MyList.Add("b");
MyList.Add("c");
MyList.Add("d");
DataContext = this;
}
}
}
When I run it and click the combobox, then a red X appears and says that The type System.Collection.Generic.List´1[System.Object] could not be converted. And the MessageBox is always empty.
You do not have INotifyPropertyChanged implemented on your MainWindow, but that may not be the only issue. I would read up on Dependency Properties and Data Binding before you really try to tinker with WPF. If you do not understand those concepts everything will be difficult and confusing.
EDIT
They are using a DependencyProperty (As you mentioned) it seems. But anyway, this is how you would implement one
public static readonly DependencyProperty SelectedItemsProperty = DependencyProperty.Register("SelectedItems", typeof(IList), typeof(MainWindow), new PropertyMetadata(null, new PropertyChangedCallback(OnSelectedItemsChanged)));
private static void OnSelectedItemsChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
{
MainWindow mainWindow = o as MainWindow;
if (mainWindow != null)
mainWindow.OnSelectedItemsChanged((IList)e.OldValue, (IList)e.NewValue);
}
protected virtual void OnSelectedItemsChanged(IList oldValue, IList newValue)
{
// Add your property changed side-effects. Descendants can override as well.
}
public IList SelectedItems
{
// IMPORTANT: To maintain parity between setting a property in XAML and procedural code, do not touch the getter and setter inside this dependency property!
get
{
return (IList)GetValue(SelectedItemsProperty);
}
set
{
SetValue(SelectedItemsProperty, value);
}
}
Notice it needs to be of type IList and you will need to cast to type string
Also, remove the Mode=TwoWay as it is not needed in your binding.
<dxe:ComboBoxEdit ItemsSource="{Binding MyList}" EditValue="{Binding SelectedItems}" >
<dxe:ComboBoxEdit.StyleSettings>
<dxe:CheckedComboBoxStyleSettings/>
</dxe:ComboBoxEdit.StyleSettings>
</dxe:ComboBoxEdit>
You also do not need INotifyPropertyChanged that was my mistake. I thought you were doing traditional binding.
EditValue property contains a list of objects, so your code in VM should look like this:
private List<object> _mySelectedList;
public List<object> MySelectedList
{
get
{
return _mySelectedList;
}
set
{
_mySelectedList = value;
this.RaisePropertyChanged("MySelectedList");
}
}
Or you can write EditValue converter, example you will find here.
I have simplified app to show my issue
When I click button, it changes Text property of ViewModel and TextBlock.Text is updated.
MainPage.xaml
<StackPanel>
<Button Click="ButtonBase_OnClick">Button to change text</Button>
<TextBlock Text="{x:Bind ViewModel.Text, Mode=OneWay}"></TextBlock>
</StackPanel>
MainPage.xaml.cs
public MainPage()
{
ViewModel = new ViewModel();
this.InitializeComponent();
}
private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
ViewModel.Text = "x:Bind works";
}
ViewModel class has one string property (Text) and implemented INotifyPropertyChange interface.
Problem starts when ViewModel is not set in ctor (i.e. viewModel is null and changed in runtime):
public MainPage()
{
//ViewModel = new ViewModel();//this line has been removed
this.InitializeComponent();
}
private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
ViewModel = new ViewModel();//this line has been added
ViewModel.Text = "x:Bind does not work";
}
Complited binding is not working (Text is not changed) and I could not figure out why it is so... I need to change viewModel from null (vm is null because it is waiting for some data in real app)
{x:Bind} bindings (often referred-to as compiled bindings) uses generated code to achieve its benefits. At XAML load time, {x:Bind} is converted into what you can think of as a binding object, and this object gets a value from a property on a data source. These generated code can be found in your obj folder, with names like (for C#) <view name>.g.cs.
For your code, the generated code will like following:
// Update methods for each path node used in binding steps.
private void Update_(global::UWP.BlankPage3 obj, int phase)
{
if (obj != null)
{
if ((phase & (NOT_PHASED | DATA_CHANGED | (1 << 0))) != 0)
{
this.Update_ViewModel(obj.ViewModel, phase);
}
}
}
private void Update_ViewModel(global::UWP.ViewModel obj, int phase)
{
this.bindingsTracking.UpdateChildListeners_ViewModel(obj);
if (obj != null)
{
if ((phase & (NOT_PHASED | DATA_CHANGED | (1 << 0))) != 0)
{
this.Update_ViewModel_Text(obj.Text, phase);
}
}
}
...
private global::UWP.ViewModel cache_ViewModel = null;
public void UpdateChildListeners_ViewModel(global::UWP.ViewModel obj)
{
if (obj != cache_ViewModel)
{
if (cache_ViewModel != null)
{
((global::System.ComponentModel.INotifyPropertyChanged)cache_ViewModel).PropertyChanged -= PropertyChanged_ViewModel;
cache_ViewModel = null;
}
if (obj != null)
{
cache_ViewModel = obj;
((global::System.ComponentModel.INotifyPropertyChanged)obj).PropertyChanged += PropertyChanged_ViewModel;
}
}
}
Here I just copy some method that related to your issue. From these method, you can find that before update TextBlock or PropertyChanged listeners, it will check if the ViewModel is null. If it is null, nothing will be done. So to make {x:Bind} work, we must initialize ViewModel before page loaded. And this is the reason why {x:Bind} doesn't work when you initialize ViewModel in Button.Click event.
To fix this issue, you can implement INotifyPropertyChanged interface for ViewModel like Filip said so that the generated code can be notified when ViewModel changed (from null to new ViewModel()) and update you UI.
But I think you can just initialize ViewModel in constructor. When you initialize ViewModel, you can set the properties that you are waiting for to null first like:
public MainPage()
{
ViewModel = new ViewModel() { Text = null };
this.InitializeComponent();
}
And then update these properties when your date is ready. In this way, you can do not implement INotifyPropertyChanged interface on your page.
Besides these, there is another cheaper way, you can call this.Bindings.Update(); method to force the bindings to be updated after you initialize ViewModel like following:
private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
ViewModel = new ViewModel();
ViewModel.Text = "x:Bind does not work";
this.Bindings.Update();
}
Did you implement INotifyPropertyChanged on page like so
public sealed partial class MainPage : Page, INotifyPropertyChanged
{
private ViewModel viewModel;
public ViewModel ViewModel
{
get { return viewModel; }
set
{
viewModel = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ViewModel)));
}
}
public MainPage()
{
ViewModel = new ViewModel { };
this.InitializeComponent();
}
private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
ViewModel = new ViewModel { };//this line has been added
ViewModel.Text = "x:Bind does not work";
}
public event PropertyChangedEventHandler PropertyChanged;
}
This works for me.
I developed an application on Windows 10 Universal App who use MVVM but I have a big problem with it.
I would add an ObservableCollection item(created on a second window) to the MVVM and then, show the new item on the ListView of MainPage but it doesn't refresh!
The 2 windows are always open
http://i.stack.imgur.com/WSo6v.jpg
The code of MVVMList.cs
public class MVVMList : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private ObservableCollection<THEFile> onglets_cache = new ObservableCollection<THEFile>();
public ObservableCollection<THEFile> onglets_list
{
get
{
return onglets_cache;
}
set
{
onglets_cache = value;
if (PropertyChanged != null)
PropertyChanged.Invoke(this,
new PropertyChangedEventArgs("onglets_list"));
}
}
public MVVMList()
{
onglets_list = new ObservableCollection<THEFile>();
Fonctions fonctions = new Fonctions();
fonctions.LoadOnglets(onglets_cache);
}
}
The code of the second page(always open) - CreateFile.xaml.cs
private void create_butt_Click(object sender, RoutedEventArgs e)
{
Fonctions fonc = new Fonctions(); MVVMList main = new MVVMList();
fonc.SetupNew(main.onglets_list, "test" + ".php", "");
}
//SetupNew on Fonctions.cs
public async void SetupNew(ObservableCollection<THEFile> list, string name, string content)
{
FolderPicker folderpick = new FolderPicker();
folderpick.ViewMode = PickerViewMode.List;
folderpick.FileTypeFilter.Add(".html"); folderpick.FileTypeFilter.Add(".htm"); folderpick.FileTypeFilter.Add(".HTML");
folderpick.FileTypeFilter.Add(".php"); folderpick.FileTypeFilter.Add(".PHP");
folderpick.FileTypeFilter.Add(".css"); folderpick.FileTypeFilter.Add(".CSS");
folderpick.FileTypeFilter.Add(".js"); folderpick.FileTypeFilter.Add(".JS");
StorageFolder storage_file = await folderpick.PickSingleFolderAsync();
if (storage_file != null)
{
MainPage vm = new MainPage();
list.Add(new THEFile { NameOfFile = name, PathOfFile = storage_file.Path + "\\" + name, CodeOfFile = content, already_opened = false, line = 0 });
string path = storage_file.Path + #"\" + name;
StorageFile file_create = await storage_file.CreateFileAsync(name, CreationCollisionOption.GenerateUniqueName);
Windows.Storage.AccessCache.StorageApplicationPermissions.FutureAccessList.Add(file_create);
SaveOnglets(list);
}
}
And on the MainPage.xaml (always open)
...
<ListView x:Name="onglets" x:FieldModifier="public" ItemTemplate="{StaticResource Templa}" ItemsSource="{Binding onglets_list}" SelectionChanged="onglets_SelectionChanged" Margin="0,117,0,57" Visibility="Visible" ContainerContentChanging="onglets_ContainerContentChanging">
...
Thank you!
In your XAML, try using a Collection View Source.
Add this to the top of your xaml:
<Page.Resources>
<CollectionViewSource x:Name="MakesCollectionViewSource" IsSourceGrouped="True"/>
</Page.Resources>
Set your ListView:
ItemsSource="{Binding Source={StaticResource MakesCollectionViewSource}}"
Then in your code when you have a List of items assign it using
MakesCollectionViewSource.Source = /* Some List<GroupInfoList<object>> that is generated from onglets_list*/
I create my List like this but it may not be relevant because this is to make all of my object names alphabetical:
internal List<GroupInfoList<object>> GetGroupsByLetter()
{
var groups = new List<GroupInfoList<object>>();
var query = from item in MakeList
orderby ((Make)item).MakeName
group item by ((Make)item).MakeName[0] into g
select new { GroupName = g.Key, Items = g };
foreach (var g in query)
{
var info = new GroupInfoList<object>();
info.Key = g.GroupName;
foreach (var item in g.Items)
{
info.Add(item);
}
groups.Add(info);
}
return groups;
}
public class GroupInfoList<T> : List<object>
{
public object Key { get; set; }
public new IEnumerator<object> GetEnumerator()
{
return (System.Collections.Generic.IEnumerator<object>)base.GetEnumerator();
}
}
where MakeList is my observable collection and Make are the objects in the collection and MakeName is a string I am trying to alphabetize
And then call using
MakesCollectionViewSource.Source = GetGroupsByLetter();
If I understand your code and requirements correctly, I think part of the problem is that you "new up" your MVVMList and your MainPage everytime you click the create button.
So, without getting into suggestions about using MVVM Light and an IOC container, you could quickly accomplish what you're trying to do by making your MVVMList class a singleton and having your MainPage use it for a data context. When your other window adds to the MVVMList.onglets collection, it will be immediately reflected in your currently open MainPage. Let me know if you need some code snippets. Good luck!
[Edit below]
I had a few minutes left on lunch, so here is an over-simplified example. Again, without getting into what MVVM is and is not. Personally, I would do this differently, but that would be outside the scope of your question. Full disclosure - this is in WPF, but same logic applies, I just don't have Windows 10 on the PC that I'm using. I also simplified the collection to be of type string. This is not intended to copy/paste into your code as it will not work in your example - but should easily transfer.
MVVMList class:
public class MVVMList: INotifyPropertyChanged
{
//Singleton section
private static MVVMList instance;
private MVVMList() { }
public static MVVMList Instance
{
get
{
if (instance == null)
{
instance = new MVVMList();
}
return instance;
}
}
//end singleton section
private ObservableCollection<string> _onglets = new ObservableCollection<string>();
public ObservableCollection<string> Onglets
{
get { return _onglets; }
set
{
if (_onglets != value)
{
_onglets = value;
if (PropertyChanged != null)
PropertyChanged.Invoke(this,
new PropertyChangedEventArgs("onglets_list"));
}
}
}
//INotify implementation
public event PropertyChangedEventHandler PropertyChanged;
}
MainPage:
<ListView x:Name="onglets" x:FieldModifier="public" ItemsSource="{Binding Onglets}" />
MainPage.cs:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.DataContext = MVVMList.Instance;
Loaded += MainWindow_Loaded;
}
void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
var x = new CreateWindow();
x.Show();
}
}
CreateWindow.cs:
private void CreateButton_Click(object sender, RoutedEventArgs e)
{
if (!string.IsNullOrEmpty(StringTextBox.Text))
{
MVVMList.Instance.Onglets.Add(StringTextBox.Text);
}
}
So here I am again, asking a very similar question to yesterday. I re-factored my project in order to better follow the MVVM pattern. Now my binding is no longer working as it was yesterday. I am trying to bind the visibility of a dock panel to a button. Here is some of my code:
ViewModel:
public class SelectWaferButtonViewModel : INotifyPropertyChanged
{
private bool isClicked;
public SelectWaferButtonViewModel()
{
isClicked = false;
}
public bool IsControlVisible
{
get
{
return isClicked;
}
set
{
isClicked = value;
OnPropertyChanged("IsControlVisible");
}
}
public event PropertyChangedEventHandler PropertyChanged;
public void OnButtonClick()
{
if (isClicked)
{
IsControlVisible = false;
}
else
{
IsControlVisible = true;
}
}
protected virtual void OnPropertyChanged(string property)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(property));
}
}
}
XAML:
<Window.Resources>
<local:BoolToVisibilityConverter x:Key="BoolToVisConverter"/>
<local:SelectWaferButtonViewModel x:Key="SelectWaferButton" />
<local:WaferTrackerWindowViewModel x:Key="WindowViewModel" />
</Window.Resources>
<DockPanel
Name="tvwDockPanel"
DataContext="{StaticResource SelectWaferButton}"
Width="225"
Visibility="{Binding IsControlVisible, Mode=TwoWay,
FallbackValue=Collapsed,
Converter={StaticResource BoolToVisConverter}}"
DockPanel.Dock="Left">
</DockPanel>
My BoolToVisConverter:
public class BoolToVisibilityConverter : IValueConverter
{
public BoolToVisibilityConverter() { }
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
bool bValue = (bool) value;
if (bValue)
{
return Visibility.Visible;
}
else
{
return Visibility.Collapsed;
}
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
Visibility visibility = (Visibility) value;
if (visibility == Visibility.Visible)
{
return true;
}
else
{
return false;
}
}
}
I apologize for a question that is similar to yesterday, but I am struggling with this MVVM stuff since I am quite new to WPF. Any help will be much appreciated.
Thanks in advanced,
EDIT:
Here is some extra code snippets for further reference:
public class WaferTrackerWindowViewModel :INotifyPropertyChanged
{
private SelectWaferButtonViewModel btnSelectWaferViewModel;
public event PropertyChangedEventHandler PropertyChanged;
private DelegateCommand exitCommand;
private DelegateCommand expandPanelCommand;
private DelegateCommand selectWaferCommand;
public WaferTrackerWindowViewModel()
{
this.InstantiateObjects();
initThread.RunWorkerAsync();
}
public string SelectedWafer
{
get
{
return selectedWafer;
}
set
{
selectedWafer = value;
}
}
public ICommand ExitCommand
{
get
{
if (exitCommand == null)
{
exitCommand = new DelegateCommand(Exit);
}
return exitCommand;
}
}
public ICommand ExpandPanelCommand
{
get
{
if (expandPanelCommand == null)
{
expandPanelCommand = new DelegateCommand(ExpandPanel);
}
return expandPanelCommand;
}
}
public ICommand SelectWaferCommand
{
get
{
if (selectWaferCommand == null)
{
selectWaferCommand = new DelegateCommand(SelectWafer);
}
return selectWaferCommand;
}
}
private void InstantiateObjects()
{
btnSelectWaferViewModel = new SelectWaferButtonViewModel();
initThread = new BackgroundWorker();
}
private void ExpandPanel()
{
btnSelectWaferViewModel.OnButtonClick();
}
private void SelectWafer()
{
//Does Nothing Yet
}
private void Exit()
{
Application.Current.Shutdown();
}
private void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
private void InitThread_DoWork(object sender, DoWorkEventArgs e)
{
TreeViewPresenter tvwPresenter = new TreeViewPresenter();
tvwPresenter.WaferList = DataLibrary.GetWaferList();
}
private void InitThread_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
tvwPresenter.TreeView.DataContext = tvwPresenter.ProcessesAndWafers;
tvwPresenter.WaferListCache = tvwPresenter.ProcessesAndWafers;
tvwPresenter.ProcessArray = tvwPresenter.WaferListCache.ToArray();
}
}
When the "expand panel" button gets clicked, it calls the ExpandPanel command, which routes the execution to the method "private void ExpandPanel()" in this same class. Then, in the ExpandPanel() method, it calls the OnButtonClick() method on the btnSelectWaferViewModel object, which will change the IsControlVisible property. This change should then be reflected onto the bound dock panel, but this is not happening
Kyle
(1) ViewModel should be in the Window.DataContext section, not the Window.Resources section.
(2) In your view model, make your IsControlVisible property a System.Windows.Visibility, rather than a Boolean, then you don't need a converter.
(3) I don't see any way for OnButtonClick to fire, and it really needs to be set up with ICommand interface.
(4) You don't need to implement ConvertBack because the Visibility property you're binding to is one way by definition. There is no way for the user to set the visibility to false.
(5) Don't mix accessing IsClicked and it's accessor IsControlVisible. Always use the Accessor in MVVM, because you run the risk of accidentally setting IsClicked which won't activate OnPropertyChanged.
All in all, you're pretty close. Make sure to keep an eye on your "Output" window, it will tell you if a binding is failing for some reason. But yeah, hang in there!
So when you do this:
<Window.Resources>
<local:SelectWaferButtonViewModel x:Key="SelectWaferButton" />
</Window.Resources>
WPF will create a new instance of the SelectWaferButtonViewModel and add it to it's resources. You then bind to this by setting the DataContext using the StaticResource with the key.
However, if you are then creating another SelectWaferButtonViewModel in your code behind and linking up your command to that instance, then it's not the same instance, so changes to the properties of this unbound instance won't effect your UI. There are a couple of ways around it. You can either a) create a single SelectWaferButtonViewModel in the code behind as a property and then bind to that in XAML, or b) Declare your SelectWaferButtonViewModel in XAML as you currently have it and then retrieve that instance in your code behind, like this:
SelectWaferButtonViewModel swbvm = (SelectWaferButtonViewModel)this.FindResource("SelectWaferButton");
Edit: So after seeing your last edit, if you want to go with a) then I would suggest you expose btnSelectWaferViewModel as a property in your WaferTrackerWindowViewModel and then bind to that property with the DataContext of your Window set to the WaferTrackerWindowViewModel instance. So you end up with something like:
<DockPanel
Name="tvwDockPanel"
Width="225"
Visibility="{Binding MyButton.IsControlVisible,
Converter={StaticResource BoolToVisConverter}}"
DockPanel.Dock="Left">
</DockPanel>
and:
public class WaferTrackerWindowViewModel :INotifyPropertyChanged
{
private SelectWaferButtonViewModel btnSelectWaferViewModel;
public SelectWaferButtonViewModel MyButton
{
get { return btnSelectWaferViewModel; }
set
{
btnSelectWaferViewModel = value;
OnPropertyChanged("MyButton");
}
}
//......
I'm writing a MVVM app and have started putting in a few animations. I want to call something on the ViewModel which starts the a storyboard. This blog had a promising approach to it, but it doesn't actually work. The IDChanged handler never fires for some reason.
I also found that you could start animations on EventTriggers, but I don't know how to raise one on the ViewModel.
I did this by a using DataTrigger and binding it to a property in my ViewModel. When the "FlashingBackGround" property gets set to "ON" the Storyboard animation starts.
Also make sure to include in your project a reference to "Microsoft.Expression.Interactions"
XAML: (this goes directly in the root node)
<Window
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
x:Name="window" >
...
<i:Interaction.Triggers>
<ei:DataTrigger Binding="{Binding FlashingBackground, Mode=OneWay}" Value="ON">
<ei:ControlStoryboardAction Storyboard="{StaticResource MyAnimation}"
ControlStoryboardOption="Play"/>
</ei:DataTrigger>
</i:Interaction.Triggers>
...
</Window>
ViewModel:
private void TurnOnFlashingBackround()
{
FlashingBackground = "ON";
}
private string _FlashingBackround = "OFF";
public string FlashingBackground
{
get { return _FlashingBackround; }
private set
{
if (FlashingBackground == value)
{
return;
}
_FlashingBackround = value;
this.OnPropertyChanged("FlashingBackground");
}
}
public new event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propertyName)
{
if (this.PropertyChanged != null)
{
this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
Finally, the Viewmodel must inherit from "INotifyPropertyChanged"
I ran into the same problem, and none of these posts really helped because the animations are in code, and some of them were large and complicated and required fluctuating variables so they had to stay in code. I resolved it by adding dependency properties in the user control (view) that trigger the animations, and binding them to properties in the view-model. Don't know (/care) if this violates something or other, because it works very well! cheers, stepp
excerpt:
(view) Usercontrol code behind:
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
SetAnimationBindings();
}
private void SetAnimationBindings()
{
_dialogStartPosition = mbFolderBrowse.Margin;
var propName = "StartDialogAnimation";
var binding = new Binding(propName) { Mode = BindingMode.TwoWay };
this.SetBinding(DialogAnimationProperty, binding);
propName = "StartProgressAnimation";
binding = new Binding(propName) { Mode = BindingMode.TwoWay };
this.SetBinding(ProgressAnimationProperty, binding);
}
#region Animation Properties
#region DialogAnimation
public static readonly DependencyProperty DialogAnimationProperty =
DependencyProperty.Register("DialogAnimation", typeof(bool),
typeof(Manage), new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnDialogAnimationChanged));
public bool DialogAnimation
{
get { return (bool)this.GetValue(DialogAnimationProperty); }
set
{
var oldValue = (bool)this.GetValue(DialogAnimationProperty);
if (oldValue != value) this.SetValue(DialogAnimationProperty, value);
}
}
private static void OnDialogAnimationChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
{
Manage m = o as Manage;
if ((bool)e.NewValue == true)
m.SlideInDialogPanel(); // animations
else
m.SlideOutDialogPanel();
}
#endregion
View-Model:
public bool StartDialogAnimation
{
get { return _startDialogAnimation; }
set
{
if (_startDialogAnimation != value)
{
_startDialogAnimation = value;
RaisePropertyChanged("StartDialogAnimation");
}
}
}
I have a property in my VM that reflects the state of the application. The elements in the view that are animated have a data trigger that starts a storyboard when the VM property has a certain value.
I ended up adding an AnimationStarted event to my ViewModel with a key string for what animation it is. Then on the view I create the animation programmatically, subscribe to the AnimationStarted event, and kick the appropriate animation off when it fires.