I have a .Net 4.5 app that is moving to WPF-based RxUI (kept up to date, 6.0.3 as of this writing). I have a text field that should function as a filter field with the fairly common throttle etc. stuff that was part of the reason for going reactive in the first place.
Here is the relevant part of my class.
public class PacketListViewModel : ReactiveObject
{
private readonly ReactiveList<PacketViewModel> _packets;
private PacketViewModel _selectedPacket;
private readonly ICollectionView _packetView;
private string _filterText;
/// <summary>
/// Gets the collection of packets represented by this object
/// </summary>
public ICollectionView Packets
{
get
{
if (_packets.Count == 0)
RebuildPacketCollection();
return _packetView;
}
}
public string FilterText
{
get { return _filterText; }
set { this.RaiseAndSetIfChanged(ref _filterText, value); }
}
public PacketViewModel SelectedPacket
{
get { return _selectedPacket; }
set { this.RaiseAndSetIfChanged(ref _selectedPacket, value); }
}
public PacketListViewModel(IEnumerable<FileViewModel> files)
{
_packets = new ReactiveList<PacketViewModel>();
_packetView = CollectionViewSource.GetDefaultView(_packets);
_packetView.Filter = PacketFilter;
_filterText = String.Empty;
this.WhenAnyValue(x => x.FilterText)
.Throttle(TimeSpan.FromMilliseconds(300)/*, RxApp.TaskpoolScheduler*/)
.DistinctUntilChanged()
.ObserveOnDispatcher()
.Subscribe(_ => _packetView.Refresh());
}
private bool PacketFilter(object item)
{
// Filter logic
}
private void RebuildPacketCollection()
{
// Rebuild packet list from data source
_packetView.Refresh();
}
}
I unit test this using Xunit.net with Resharper's test runner. I create some test data and run this test:
[Fact]
public void FilterText_WhenThrottleTimeoutHasPassed_FiltersProperly()
{
new TestScheduler().With(s =>
{
// Arrange
var fvm = GetLoadedFileViewModel();
var sut = new PacketListViewModel(fvm);
var lazy = sut.Packets;
// Act
sut.FilterText = "Call";
s.AdvanceToMs(301);
// Assert
var res = sut.Packets.OfType<PacketViewModel>().ToList();
sut.Packets.OfType<PacketViewModel>()
.Count().Should().Be(1, "only a single packet should match the filter");
});
}
I put a debug statement on the Subscribe action for my FilterText config in the constructor of the class, and it gets called once for each packet item at startup, but it never gets called after I change the FilterText property.
Btw, the constructor for the test class contains the following statement to make threading magic work:
SynchronizationContext.SetSynchronizationContext(new SynchronizationContext());
My problem is basically that the Refresh() method on my view never gets called after I change the FilterText, and I can't see why not.
Is this a simple problem with my code? Or is this a problem with a CollectionViewSource thing running in a unit testing context rather than in a WPF context?
Should I abandon this idea and rather have a ReactiveList property that I filter manually whenever a text change is triggered?
Note: This works in the application - the FilterText triggers the update there. It just doesn't happen in the unit test, which makes me wonder whether I am doing it wrong.
EDIT: As requested, here are the relevant bits of XAML - this is for now just a simple window with a textbox and a datagrid.
The TextBox:
<TextBox Name="FilterTextBox"
Grid.Column="1"
VerticalAlignment="Center"
Text="{Binding FilterText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
/>
The datagrid:
<DataGrid ItemsSource="{Binding Path=Packets}"
Name="PacketDataGrid"
SelectedItem="{Binding SelectedPacket}"
AutoGenerateColumns="False"
EnableRowVirtualization="True"
SelectionMode="Single"
SelectionUnit="FullRow"
CanUserAddRows="False"
CanUserResizeRows="False"
>
<DataGrid.Columns>
...
If anything else is relevant/needed, let me know!
EDIT 2: Paul Betts recommends not doing the SynchronizationContext setup in the test constructor like I do, probably for very valid reasons. However, I do this because of the way another viewmodel (FileViewModel) works - it needs to wait for a MessageBus message to know that packet processing is complete. This is something that I am working actively on trying to avoid - I know the MessageBus is a very convenient bad idea. :) But this is the cause for the SyncContext stuff. The method that creates the test viewmodel looks like this:
private FileViewModel GetLoadedFileViewModel()
{
var mre = new ManualResetEventSlim();
var fvm = new FileViewModel(new MockDataLoader());
MessageBus.Current
.Listen<FileUpdatedPacketListMessage>(fvm.MessageToken.ToString())
.Subscribe(msg => mre.Set());
fvm.LoadFile("irrelevant.log");
mre.Wait(500);
return fvm;
}
I realize this is bad design, so please don't yell. ;) But I am taking a lot of legacy code here and moving it into RxUI based MVVM - I can't do it all and end up with a perfect design just yet, which is why I am getting unit tests in place for all this stuff so that I can do Rambo refactoring later. :)
Btw, the constructor for the test class contains the following statement to make threading magic work:
Don't do this
My problem is basically that the Refresh() method on my view never gets called after I change the FilterText, and I can't see why not.
I believe your problem is the commented out part:
.Throttle(TimeSpan.FromMilliseconds(300)/, RxApp.TaskpoolScheduler/)
And this part:
.ObserveOnDispatcher()
When you use TestScheduler, you must use RxApp.[MainThread/Taskpool]Scheduler for all scheduler parameters. Here above, you're using a real TaskpoolScheduler and a real Dispatcher. Since they're not under TestScheduler, they can't be controlled by TestScheduler.
Instead, write:
this.WhenAnyValue(x => x.FilterText)
.Throttle(TimeSpan.FromMilliseconds(300), RxApp.TaskpoolScheduler)
.DistinctUntilChanged()
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(_ => _packetView.Refresh());
and everything should work.
Related
I have a requirement to turn off a certain feature when the user starts typing which is simple. And when the user stops typing, I want to turn the feature back on.
Without reactive extensions, one can simply implement this feature with a timer that resets the timer on every last keystroke to 1 second and when the user stops typing and timer elapses the feature is turned back on.
Is there any method I can call to achieve the same effect with Reactive Extensions?
Throttle or Timeout keeps calling the subscriber/exception action every 1 second
UPDATE
XAML
<RichTextBox MaxHeight="1000" VerticalScrollBarVisibility="Visible" x:Name="meh"/>
Extension class
public static IObservable<EventArgs> ObserveTextChanged(this RichTextBox rtb)
{
return Observable.FromEventPattern<TextChangedEventHandler, EventArgs>(
h => rtb.TextChanged += h,
h => rtb.TextChanged -= h)
.Select(ep => ep.EventArgs);
}
Class where meh is the RichTextBox
public class MainWindow()
{
//Change this to be the keypress/propertychagned event. The type T doesn't matter we ignore it
var typing = meh.ObserveTextChanged().Take(4);
var silence = meh.ObserveTextChanged().IgnoreElements();
var source = typing.Concat(silence).Concat(typing);
var disableSpellcheck = source.Select(_ => false);
var enableSpellcheck = source.Select(_ => Observable.Timer(TimeSpan.FromSeconds(1)))
.Switch()
.Select(_ => true);
disableSpellcheck.Merge(enableSpellcheck)
.DistinctUntilChanged()
.Subscribe(SetFlag);
}
// Define other methods and classes here
public void SetFlag(bool flag)
{
Dispatcher.Invoke(new Action(() => SpellCheck.SetIsEnabled(meh, flag)));
Debug.Write("flag");
}
Here is all the code to show how the code above could be ported to WPF. It appears that there is a gap in communication here, so I have created the whole wpf app to prove the point.
<Window x:Class="StackoverFlow_23764884.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">
<StackPanel>
<HeaderedContentControl Header="When checked, spellcheck is enabled (emulated)">
<CheckBox x:Name="spellChecking" IsChecked="True" IsEnabled="False"/>
</HeaderedContentControl>
<HeaderedContentControl Header="Type here to see the Spellcheck enable and disable">
<RichTextBox x:Name="meh" Width="400" Height="300" />
</HeaderedContentControl>
</StackPanel>
</Window>
And the code behind:
using System;
using System.Reactive.Linq;
using System.Windows;
using System.Windows.Controls;
namespace StackoverFlow_23764884
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
var source = meh.ObserveTextChanged();
var disableSpellcheck = source.Select(_ => false);
var enableSpellcheck = source.Select(_ => Observable.Timer(TimeSpan.FromSeconds(1)))
.Switch()
.Select(_ => true);
disableSpellcheck.Merge(enableSpellcheck)
.DistinctUntilChanged()
.ObserveOnDispatcher()
.Subscribe(isEnabled => spellChecking.IsChecked=isEnabled);
}
}
public static class ObEx
{
public static IObservable<EventArgs> ObserveTextChanged(this RichTextBox rtb)
{
return Observable.FromEventPattern<TextChangedEventHandler, EventArgs>(
h => rtb.TextChanged += h,
h => rtb.TextChanged -= h)
.Select(ep => ep.EventArgs);
}
}
}
I pulled in the Rx-WPF nuget packeage will pulled everything else required for the code. This is .NET 4.5.
This is a sample to show how to solve the problem. i.e. I don't recommend using .ObserveOnDispatcher(), I dont recommend writing code-behind, and I know that setting IsEnabled on a checkbox is not actually doing a spellcheck. I am hoping that this is enough to the audience to recreate their actual solution.
I do hope it helps though.
Great question.
There are probably many ways to solve this, but here is one that you can work with. First we need the source observable sequence. This is probably a keypressed event or property changed event that has been converted to an Observable sequence using FromEvent or some other factory/conversion or maybe ReactiveUI.
In this sample I will use Observable.Interval(TimeSpan.FromSeconds(0.25)).Take(4); as a substitute for the source sequence (just to prove the concept).
Next, we need to decide when we need to disable the feature (SpellCheck?). This is when the source sequence yields a value.
var disableSpellcheck = source.Select(_=>false);
Then we need to decide when we need to re-enable the feature. This is when there has been 1 second of silence on the source sequence. One trick you can do to implement this is to create a One Second timer for each event from the source. When a new Timer is created, cancel the previous one. You can do this by creating a nested observable sequence, and using Switch to cancel previous inner sequences when a new one it produced.
var enableSpellcheck = source.Select(_=>Observable.Timer(TimeSpan.FromSeconds(1)))
.Switch()
.Select(_=>true);
Now we want to merge these two sequences, and push the result to method that enables/disables the feature.
Observable.Merge(disableSpellcheck, enableSpellcheck)
.Subscribe(isEnabled=>SetFlag(isEnabled));
However, as noted above, this call SetFlag(false) every time the source sequence yileded a value. This is easily solved by using the DistinctUntilChanged() operator.
The final sample (LinqPad) code is as follows:
void Main()
{
//Change this to be the keypress/propertychagned event. The type T doesn't matter we ignore it
var typing = Observable.Interval(TimeSpan.FromSeconds(0.25)).Take(4);
var silence = Observable.Timer(TimeSpan.FromSeconds(1)).IgnoreElements();
var source = typing.Concat(silence).Concat(typing);
var disableSpellcheck = source.Select(_=>false);
var enableSpellcheck = source.Select(_=>Observable.Timer(TimeSpan.FromSeconds(1)))
.Switch()
.Select(_=>true);
Observable.Merge(disableSpellcheck, enableSpellcheck)
.DistinctUntilChanged()
.Subscribe(isEnabled=>SetFlag(isEnabled));
}
// Define other methods and classes here
public void SetFlag(bool flag)
{
flag.Dump("flag");
}
I believe this example with Throttle solves a similar problem.
So something along the lines might work for you too:
var throttled = observable.Throttle(TimeSpan.FromMilliseconds(1000));
using (throttled.Subscribe(x => RestoreThing())) {}
I'm no RX master, but I've created a sample WinForms app that seems to work. On the form, I have textBox1 and button1 and that's it.
Here's the code-behind:
public Form1()
{
InitializeComponent();
var observable = Observable.FromEventPattern(
s => textBox1.TextChanged += s, s => textBox1.TextChanged -= s)
//make sure we are on the UI thread
.ObserveOn(SynchronizationContext.Current)
//immediately set it to false
.Do(_ => UpdateEnabledStatus(false))
//throttle for one second
.Throttle(TimeSpan.FromMilliseconds(1000))
//again, make sure on UI thread
.ObserveOn(SynchronizationContext.Current)
//now re-enable
.Subscribe(_ => UpdateEnabledStatus(true));
}
private void UpdateEnabledStatus(bool enabled)
{
button1.Enabled = enabled;
}
This works as you want, and does not hit the UpdateEnabledStatus method every second, at least in my testing.
(Question modified for better clarity)
I've been researching all day and nothing really clears up my issue. I have a combobox that gets its source from a collection of a "Bonus" that has a Name and a Code.
<ComboBox Margin="4"
SelectedItem="{Binding Path=SelectedBonus, UpdateSourceTrigger=PropertyChanged}"
ItemsSource="{Binding Path=Bonuses, UpdateSourceTrigger=Explicit}"
DisplayMemberPath="Name"
IsSynchronizedWithCurrentItem="True">
"Bonuses" is a collection that is retrieved from a file initially. The combobox is properly filled with the collection, and I can select any of the items in the list.
Can't post image, so here is the link to the loaded list on startup: https://www.flickr.com/photos/mav_2007/14059399414/in/set-72157644402662271
Now, the following code is called on a context switch (right click the combo-box and select "edit Bonus"). However, here is where I cannot make any sense of what's going on with WPF and bindings.
In the code below, the method called "EditBonus" resets the combobox list be re-reading the file to make sure nothing has changed (Bonuses.Clear() is called, then rebuilt, and SelectedItem set). But, as soon as I make the call to ShowDialog, the combobox selected item goes blank. If I un-comment the OnPropertyChanged calls below, the problem appears fixed. But, I do not understand why that works, and there is a case I can't fix where an exception in the Save operation of the dialog makes that combo box disappear again.
/// <summary>
/// Handle the "edit bonus" command
/// </summary>
void OnEditBonus()
{
// only edit this is we have a bonus selected
if (bonusEditViewModel.EditBonus())
{
// OnPropertyChanged("SelectedBonus");
var window = new BonusEditDialog(bonusEditViewModel);
window.ShowDialog();
// OnPropertyChanged("SelectedBonus");
}
}
And the method EditBonus():
internal bool EditBonus()
{
bool success = false;
if (SelectedBonus != null)
{
var originalCode = SelectedBonus.Code;
success = UpdateBonuses(originalCode);
if (success)
{
if (SelectedBonus.Code == originalCode)
{
BonusName = SelectedBonus.Type;
originalBonusName = BonusName;
CloseWindow = false;
}
}
}
return success;
}
Now, I've tried changing the UpdateSourceTrigger types and the modes and nothing makes a difference.
I believe it is related to the changing of the datacontext when accessing the dialog, and yet still making modification to the combobox's itemSource. But I'm not sure how I can defer the combobox update until the dialog exits.
This is what it looks like if I try to edit the list without calling that OnPropertyChanged after returning from EditBonus() (note the empty combobox):
https://www.flickr.com/photos/mav_2007/14035815226/in/set-72157644402662271
And this is what it looks like if I make the call just after returning from EditBonus():
And I would love to show you this, but I guess I'm not "reputable" enough :( (You can see all images for this issue on my Flickr page from that link)
Thanks for any help you can give
Additional information (Possible culprit identified):
The DataContext for SelectedBonus is BehaviorViewModel, but there is another SelectedBonusProperty inside BonusEditViewModel. Both BehaviorViewModel and BonusEditViewModel derive from ViewModelBase, which is where the OnPropertyChanged handler actually executes the handler based on ViewModel type. BehaviorViewModel is what is bound to the form. Calling OnPropertyChanged inside BonusEditViewModel has no effect because it is a different context.
Here is the SelectedBonus property inside BehaviorViewMode:
/// <summary>
/// Expose the bonus information from the bonusEditViewModel
/// </summary>
public Bonus SelectedBonus
{
get
{
return bonusEditViewModel.SelectedBonus;
}
set
{
bonusEditViewModel.SelectedBonus = value;
if (behavior != null && bonusEditViewModel.SelectedBonus != null)
{
behavior.Items[BehaviorItem.THEME_ID].Value = bonusEditViewModel.SelectedBonus.Code.ToString();
}
if (bonusEditViewModel.SelectedBonus != null)
{
ThemeShortname = bonusEditViewModel.SelectedBonus.Type;
}
OnPropertyChanged("SelectedBonus");
}
}
And here is the code for the SelectedBonus inside BonusEditViewModel:
/// <summary>
/// The currently selected bonus which will be edited by the dialog
/// </summary>
public Bonus SelectedBonus
{
get { return selectedBonus; }
set
{
selectedBonus = value;
OnPropertyChanged("SelectedBonus");
}
}
The function OnEditBonus() exists inside the BehaviorViewModel, and it calls the method UpdateBonuses() on the BonusEditViewModel object. Now I'm wondering the best way to fix this...
The problem was verified to be with the structure of the code, which had a binding to the property SelectedBonus in the ViewModel class called BehaviorViewModel. However, the property in this class actually gets its data from a property in the ViewModel class called BonusEditViewModel. Calling "OnPropertyChanged("SelectedBonus") inside the BonusEditViewModel class has no effect on the property bound in BehaviorViewModel. The code needs to be restructured so that the SelectedBonus property in BehaviorViewModel does not reference the property in another ViewModel. This is just poorly structured code.
I have window "ClientsWindow" and it's view model class "ClientsViewModel". In ViewModel i defined property "Clients" and bound it to DataGrid's itemssource property:
private ObservableCollection<tblClient> clients;
public ObservableCollection<tblClient> Clients
{
get { return clients; }
set
{
clients = value;
OnPropertyChanged("Clients");
}
}
In my window's constructor I set this property to new value by calling the method from wcf service like this:
Clients = new ObservableCollection<tblClient>(wcf.FilterClients(PageIndex, PageSize));
And it works perfect, I get 10 records from wcf service as it should be and the list is shown in datagrid. I insert some usercontrol which I want to use for datagrid pagination. It has ChangedIndexCommand defined like this:
ChangedIndexCommandProperty =
DependencyProperty.Register("ChangedIndexCommand", typeof(ICommand), typeof(GridPaging), new UIPropertyMetadata(null));
public ICommand ChangedIndexCommand
{
get { return (ICommand)GetValue(ChangedIndexCommandProperty); }
set { SetValue(ChangedIndexCommandProperty, value); }
}
I tried to bind command form my window's viewmodel to this command, so i did it this way:
private ICommand _cmdChangedIndex;
public ICommand cmdChangedIndex
{
get
{
if (_cmdChangedIndex == null)
{
_cmdChangedIndex = new DelegateCommand(delegate()
{
worker.DoWork += worker_FilterClientsList;
worker.RunWorkerCompleted += worker_FilterClientListCompleted;
worker.RunWorkerAsync();
});
}
return _cmdChangedIndex;
}
}
private void worker_FilterClientsList(object sender, DoWorkEventArgs e)
{
try
{
ServiceClient wcf = new ServiceClient();
Clients = new ObservableCollection<tblClient>(wcf.FilterClients(PageIndex, PageSize));
TotalCount = wcf.ReturnClientsCount();
}
catch (Exception ex)
{
}
}
private void worker_FilterClientListCompleted(object sender, RunWorkerCompletedEventArgs e)
{
worker.DoWork -= worker_FilterClientsList;
}
And here is the xaml:
<pc:GridPaging PageIndex="{Binding PageIndex, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
PageSize="{Binding PageSize, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
TotalCount="{Binding TotalCount, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
HorizontalAlignment="Center" x:Name="clientsPagingControl"
ChangedIndexCommand="{Binding cmdChangedIndex, UpdateSourceTrigger=PropertyChanged}"
Visibility="Visible" VerticalAlignment="Top"
/>
So, while debugging everything works perfect! My command is fired when i click on the button of my userconrol, the method from wcf service is called properly and it returns new collection of items(count 2, as expected), my "Clients" property is set to new value BUT, UI still showing 10 items in my datagrid. I just cant figure out what is wrong?! Is this wrong way of binding commands to custom user controls?? Also let me note that, PageIndex, PageSize and TotalCount properties are of type int, and i bound them to my viewmodel properties, and they work perfect. But what is the problem with my command? I tried to be as clear as I could hope that you will understand what my problem is, and for any more info, please leave the comment.
OnPropertyChanged:
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
DataGrid binding:
<DataGrid IsReadOnly="True" Name="dgClients" AutoGenerateColumns="False" ItemsSource="{Binding Path=Clients, UpdateSourceTrigger=PropertyChanged}">
<DataGrid.Columns>
....
</DataGrid.Columns>
</DataGrid>
Just a thought, but it looks like you are using a BackgroundWorker class in your ICommand? In the worker_FilterClientsList method, you are setting the "Clients" observable collection property. I don't think you are able to manipulate the UI from within DoWork (it's running on a different thread). Try removing the try..catch block to see if it's hiding such an error.
You normally have to update the UI from the RunWorkerCompleted delegate (your worker_FilterClientListCompleted method).
Ok, so judging by your question, answers and the many comments, it would seem that your problem is un-reproducible. This means that you are on your own, as far as fixing your problem goes. However, this is not as bad as it sounds.
As there is no obvious problem with your displayed code, I cannot point out where your error lies. However, I can put you onto the right path to fix your own problem. It will take some time and effort on your part, but 'no pain... no gain', as they say.
One of the best ways that you find the problem in a complex project is to simplify it in a new, empty project. Normally when doing this, one of two things happens: either you find out what the problem was, or you create a concise working example that demonstrates your problem, which you can then post here (maybe as a new question, or instead of your current code). It's usually a win-win situation.
As it happens, the StackOverflow Help Center has a page to help with this. Please follow the advice in the How to create a Minimal, Complete, Tested and Readable example page to help you to simplify your problem.
One final point that I'd like to make is that normally in an application, the data access layer is separate from the UI. If you separate your different concerns like this, you will also find that it simplifies the situation further.
I have chunks of XAML displayed on my screen that I make printable with a print button inside that chunk, something like this:
<Border DockPanel.Dock="Top" x:Name="PrintableArea">
<StackPanel
HorizontalAlignment="Right"
VerticalAlignment="Bottom">
<ContentControl Background="Green" x:Name="ManageButtonsContainer"/>
<Button x:Name="PrintButton" Content="Print" Click="Button_Click_Print"/>
</StackPanel>
</Border>
But when this chunk prints out, I don't want the print button to be printed, so I hide it before I print and make it visible again after I print, like this:
private void Button_Click_Print(object sender, RoutedEventArgs e)
{
PrintButton.Visibility = Visibility.Collapsed;
PrintDialog dialog = new PrintDialog();
if (dialog.ShowDialog() == true)
{ dialog.PrintVisual(PrintableArea, "Print job"); }
PrintButton.Visibility = Visibility.Visible;
}
This works, but when the print dialog appears, you see behind the print dialog that the print button disappears and then reappears again, which is just a little unconventional UI behavior that I would like to avoid.
Is there a way to keep elements visible on the screen yet hide them from printing?
e.g. something like this (pseudo-code):
<Button x:Name="PrintButton" Content="Print"
HideWhenPrinting=True"
Click="Button_Click_Print"/>
Pragmatic answer:
Ok, I solved this particular issue simply by changing the visibility only if they actually print, but it would still be nice to know in principle if there is a way to set "printable visibility" in XAML so this issue doesn't always have to be taken care of in code like this:
private void Button_Click_Print(object sender, RoutedEventArgs e)
{
PrintDialog dialog = new PrintDialog();
if (dialog.ShowDialog() == true)
{
PrintButton.Visibility = Visibility.Collapsed;
dialog.PrintVisual(PrintableArea, "Print job");
PrintButton.Visibility = Visibility.Visible;
}
}
I couldn't find any easy answer for your question, so I decided to scary everybody who reads this with the huge code below. It creates attached property, called PrintExtension.IsPrintable, and every time you set it to true on an item, it starts "tracking" that item. Before printing one should call PrintExtension.OnBeforePrinting(), and when you are done call PrintExtension.OnAfterPrinting(). It does exactly the same thing you have in your code, but more effortless.
/// <summary>
/// Hides PrintExtensions.IsPrintable="False" elements before printing,
/// and get them back after. Not a production quality code.
/// </summary>
public static class PrintExtensions
{
private static readonly List<WeakReference> _trackedItems = new List<WeakReference>();
public static bool GetIsPrintable(DependencyObject obj)
{
return (bool)obj.GetValue(IsPrintableProperty);
}
public static void SetIsPrintable(DependencyObject obj, bool value)
{
obj.SetValue(IsPrintableProperty, value);
}
public static readonly DependencyProperty IsPrintableProperty =
DependencyProperty.RegisterAttached("IsPrintable",
typeof(bool),
typeof(PrintExtensions),
new PropertyMetadata(true, OnIsPrintableChanged));
private static void OnIsPrintableChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var printable = (bool)e.NewValue;
bool isTracked = IsTracked(d);
if (printable && !isTracked)
{
StartTracking(d);
}
else if (!printable && isTracked)
{
StopTracking(d);
}
}
/// <summary>
/// Call this method before printing.
/// </summary>
public static void OnBeforePrinting()
{
IterateTrackedItems(
item =>
{
var fe = item.Target as FrameworkElement;
if (fe != null)
{
fe.Visibility = Visibility.Collapsed; // Boom, we break bindings here, if there are any.
}
});
}
/// <summary>
/// Call this after printing.
/// </summary>
public static void OnAfterPrinting()
{
IterateTrackedItems(
item =>
{
var fe = item.Target as FrameworkElement;
if (fe != null)
{
fe.Visibility = Visibility.Visible; // Boom, binding is broken again here.
}
});
}
private static void StopTracking(DependencyObject o)
{
// This is O(n) operation.
var reference = _trackedItems.Find(wr => wr.IsAlive && wr.Target == o);
if (reference != null)
{
_trackedItems.Remove(reference);
}
}
private static void StartTracking(DependencyObject o)
{
_trackedItems.Add(new WeakReference(o));
}
private static bool IsTracked(DependencyObject o)
{
// Be careful, this function is of O(n) complexity.
var tracked = false;
IterateTrackedItems(
item =>
{
if (item.Target == o)
{
tracked = true;
}
});
return tracked;
}
/// <summary>
/// Iterates over tracked items collection, and perform eachAction on
/// alive items. Don't want to create iterator, because we do house
/// keeping stuff here. Let it be more prominent.
/// </summary>
private static void IterateTrackedItems(Action<WeakReference> eachAction)
{
var trackedItems = new WeakReference[_trackedItems.Count];
_trackedItems.CopyTo(trackedItems);
foreach (var item in trackedItems)
{
if (!item.IsAlive) // do some house keeping work.
{
_trackedItems.Remove(item); // Don't care about GC'ed objects.
}
else
{
eachAction(item);
}
}
}
}
NB: I haven't tested this code. Be careful with it. As you can see, it's far from being perfect, and I really hope there is simpler solution.
Cheers, Anvaka.
This question is closely related to the one you'd asked an hour earlier (some six years ago, I realize…but I find the answers so far to both unsatisfactory, having recently stumbled across these questions myself, hence these answers). That is, a big part of the problem in both is that you are attempting to use the objects you're displaying on the screen for the purpose of printing, when in fact you should be taking advantage of WPF's data templating features to address your concerns.
In this particular example, you could approach the problem in a few different ways:
Declare a single DataTemplate for on-screen and printing purposes. Including in the view model a flag indicating whether the object is being printed or not. Make a copy of the view model when printing, except set the "is printing" flag to true. In the template, bind the visibility of the Button to this flag (i.e. use a converter to set Visibility="Collapsed" if the flag is true, or define a Trigger that will do the same thing).
Do the above, but instead of including a flag in the view model, when you are printing the data, just explicitly search the visual tree for the Button after the ControlControl has loaded its templated content and collapse the Button before you print the control.
Declare a separate template specifically for the purpose of printing the view model data, and just leave the Button out in that template. This would give you the most control over printing-specific behaviors and appearances, but at the added cost of having to maintain two different-but-related templates.
In all three options, as well as other variations on that theme, the key is that you would use the data templating features to cause WPF to populate a new visual tree to go along with the view model object you're dealing with. In this way, you avoid unwanted interactions between the needs of the printing code and what's happening on the screen (something that is not true for the other answer posted here).
That said, all of these three options have their drawbacks. Copying a view model (per option #1) is fine if it's simple, but it could get unwieldy for more complex data structures. Digging into the generated content (option #2) has obvious negative ramifications, and of course maintaining two different templates (option #3) is just a pain (which in some, but not all cases, could be mitigated by incorporating the "print" template inside the "screen" template via a ContentControl, depending on how important the ordering of the controls is).
Having spent more time dealing with this question in my own code, I've come to the conclusion that, while a bit on the "hacky" side, the solution that works best for me is to set a Trigger that is based on searching for an ancestor element that would be present on the screen, but not when the data template is loaded in a ContentControl. E.g., in the DataTemplate, something like this:
<Button Content="Print" Click="Button_Click_Print">
<Button.Style>
<p:Style TargetType="Button">
<p:Style.Triggers>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource AncestorType=Window}}" Value="{x:Null}">
<Setter Property="Visibility" Value="Collapsed"/>
</DataTrigger>
</p:Style.Triggers>
</p:Style>
</Button.Style>
</Button>
I wouldn't say it's 100% kosher for the template to be so self-aware. I prefer they be more agnostic about the context in which they're being used. But you have a fairly unique situation here, one in which being self-aware is going to need to happen one way or the other. I have found this approach to be the least of all possible evils. :)
(Sorry about the p:Style stuff…it's just a XAML-compliant way to work-around the bug in the XML-formatting code Stack Overflow uses, so that code coloring continues to work inside the Style element. You can leave the p: namespace qualifier out in your own XAML if you like, or just go ahead and declare the p: XML namespace appropriately.)
I'm developing WPF applications using MVVM pattern. I have ViewModel with code like this:
public bool EditModeEnabled
{
get { return _EditModeEnabled; }
set
{
_ModeEditModeEnabled = value;
OnPropertyChanged("EditModeEnabled");
OnPropertyChanged("CommentTextBoxVisibility");
}
}
OnPropertyChanged is virtual method of base class which just raise PropertyChanged event.
I want to test PropertyChanged event raising and there my test method:
public void EditModeEnabledTest()
{
var imageViewModel = TestHelper.GetTestImageViewModel();
var firedEvents = new List<string>();
imageViewModel.PropertyChanged += ((sender, e) => firedEvents.Add(e.PropertyName));
imageViewModel.Mode = true;
Assert.AreEqual(firedEvents.Count, 2);
Assert.IsTrue(firedEvents.Contains("EditModeEnabled"));
Assert.IsTrue(firedEvents.Contains("CommentTextBoxVisibility"));
...
}
Is it a good way to test ProprtyChanged event?
I use a little Fluent API for doing exactly that. It allows you to write tests like this:
var imageViewModel = TestHelper.GetTestImageViewModel();
imageViewModel.ShouldNotifyOn(s => s.EditModeEnabled)
When(s => s.Mode = true);
Besides being succinct, I prefer this approach because it's type-safe - no string values to keep in sync with your API.
To test that the event is being raised for more than one property, you can just write another test that does this. This will give you many tests, but each will be very small and you avoid Assertion Roulette.
I believe it’s a good idea to unit test the PropertyChanged event in the example you have shown. You might have written the property name string wrong which would result in a missing update.
With the WPF Application Framework (WAF) it’s very easy to write such a unit test:
Person person = new Person();
AssertHelper.PropertyChangedEvent(person, x => x.Name, () => person.Name = "Luke");
Similar to the suggestion of Mark Seemann, with Fluent Assertions you have simple nice methods that wrap it all up for you, like:
subject.Should().Raise().PropertyChangeFor(x => x.SomeProperty);
Source
https://fluentassertions.com/eventmonitoring/