1

I have a form window I show with a ShowDialog(), the textboxes are bound to properties in the view model. I open my dialog like this (simplified version):

FilterWindowView wnd = new FilterWindowView();
FilterWindowViewModel fvm = new FilterWindowViewModel(licenseRecords) { wnd = wnd };
wnd.DataContext = fvm;
fvm.RestoreCurrentFilters();
if (wnd.ShowDialog() ?? false)
{
    //...
}

The properties I set in my form are used as filter parameters, which I store in a static class to retrieve for later usage.

What I would want to do, is to have the textboxes autofill with the current value stored in this static class.

My textbox bound properties look like this:

    private string _product;
    public string product
    {
        get { return _product; }
        set
        {
            if (_product == value)
                return;
            _product = value;
            Helper.product = value;
            if (value != "")
                chkProduct = true;
            OnPropertyChanged();
        }
    }

(I think it may be better performance wise to reassign when validating but this is another question...) My problem here is that if I set a value (i.e. in the constructor), the value gets set but when calling ShowDialog(), the value is reset to "".

Also tried calling a method after instantiating the VM, but as said, this reset happens when showing the window ( when calling ShowDialog())...

This form generates a custom object I recover in the VM dialogResult so going wnd.Show() and then setting to stored values is not an option for me (I guess?).

Thanks for any help.

EDIT View is as simple as it gets, just a few labels and textboxes two way bound to the VM.

<Window x:Class="LicenseManager.View.FilterWindowView"
    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:LicenseManager.View"
    mc:Ignorable="d"
    Title="FilterWindowView" Height="306.412" Width="284.216">
<Grid>
    <CheckBox Content="Product" HorizontalAlignment="Left" Margin="10,10,0,0" VerticalAlignment="Top" IsChecked="{Binding chkProduct}"/>
    <TextBox HorizontalAlignment="Left" Height="23" Margin="137,8,0,0" TextWrapping="Wrap" Text="{Binding product, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" VerticalAlignment="Top" Width="120"/>
    <CheckBox Content="Client" HorizontalAlignment="Left" Margin="10,38,0,0" VerticalAlignment="Top" IsChecked="{Binding chkClient}"/>
    <TextBox HorizontalAlignment="Left" Height="23" Margin="137,36,0,0" TextWrapping="Wrap" Text="{Binding client, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" VerticalAlignment="Top" Width="120"/>
    <CheckBox Content="Date After" HorizontalAlignment="Left" Margin="10,66,0,0" VerticalAlignment="Top" IsChecked="{Binding chkDateAfter}"/>
    <TextBox HorizontalAlignment="Left" Height="23" Margin="137,64,0,0" TextWrapping="Wrap" Text="{Binding dateAfter, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" VerticalAlignment="Top" Width="120"/>
    <CheckBox Content="Date Before" HorizontalAlignment="Left" Margin="10,94,0,0" VerticalAlignment="Top" IsChecked="{Binding chkDateBefore}"/>
    <TextBox HorizontalAlignment="Left" Height="23" Margin="137,92,0,0" TextWrapping="Wrap" Text="{Binding dateBefore, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" VerticalAlignment="Top" Width="120"/>
    <CheckBox Content="Sbs__no" HorizontalAlignment="Left" Margin="10,122,0,0" VerticalAlignment="Top" IsChecked="{Binding chkSbsNo}"/>
    <TextBox HorizontalAlignment="Left" Height="23" Margin="137,120,0,0" TextWrapping="Wrap" Text="{Binding sbsNo, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" VerticalAlignment="Top" Width="120"/>
    <CheckBox Content="Store__no" HorizontalAlignment="Left" Margin="10,150,0,0" VerticalAlignment="Top" IsChecked="{Binding chkStoreNo}"/>
    <TextBox HorizontalAlignment="Left" Height="23" Margin="137,148,0,0" TextWrapping="Wrap" Text="{Binding storeNo, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" VerticalAlignment="Top" Width="120"/>
    <CheckBox Content="Workstation__no" HorizontalAlignment="Left" Margin="10,178,0,0" VerticalAlignment="Top" IsChecked="{Binding chkWorkstationNo}"/>
    <TextBox HorizontalAlignment="Left" Height="23" Margin="137,176,0,0" TextWrapping="Wrap" Text="{Binding workstationNo, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" VerticalAlignment="Top" Width="120"/>
    <CheckBox Content="Comment" HorizontalAlignment="Left" Margin="10,206,0,0" VerticalAlignment="Top" IsChecked="{Binding chkWorkstationNo}"/>
    <TextBox HorizontalAlignment="Left" Height="23" Margin="137,204,0,0" TextWrapping="Wrap" Text="{Binding comment, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" VerticalAlignment="Top" Width="120"/>
    <Button Content="Apply" Command="{Binding apply}" HorizontalAlignment="Left" Margin="10,236,0,0" VerticalAlignment="Top" Width="75"/>
</Grid>

EDIT The View Model class Helper is my static class

    class FilterWindowViewModel : INotifyPropertyChanged
        {
            #region Attributes
            public Window wnd; // For dialog closer
            public List<LicenseRecordModel> list;
            public List<LicenseRecordModel> dialogResult;
            public event PropertyChangedEventHandler PropertyChanged;

            string tmpProduct;
            string tmpClient;
            string tmpDateAfter;
            string tmpDateBefore;
            string tmpSbsNo;
            string tmpStoreNo;
            string tmpWorkstationNo;
            string tmpComment;
            #endregion

            #region Properties
            //Properties and commands
        private string _comment;
        public string comment
        {
            get { return _comment; }
            set
            {
                if (_comment == value)
                    return;
                _comment = value;
                Helper.comment = value;
                if (value != "")
                    chkComment = true;
                OnPropertyChanged();
            }
        }


//...
        private DelegateCommand _apply;
        public DelegateCommand apply
        {
            get
            {
                return _apply ?? (_apply = new DelegateCommand(o => Apply(), o => true));
            }
        }
            #endregion

            #region Init
            public FilterWindowViewModel(IEnumerable<LicenseRecordModel> source)
            {
                tmpProduct = Helper.product;
                tmpClient = Helper.client;
                tmpDateAfter = Helper.dateAfter;
                tmpDateBefore= Helper.dateBefore;
                tmpSbsNo = Helper.sbsNo;
                tmpStoreNo = Helper.storeNo;
                tmpWorkstationNo = Helper.workstationNo;
                tmpComment = Helper.comment;
                list = new List<LicenseRecordModel>(source);
            }

            public void RestoreCurrentFilters()
            {
                product = tmpProduct;
                client = tmpClient;
                dateAfter = tmpDateAfter;
                dateBefore = tmpDateBefore;
                sbsNo = tmpSbsNo;
                storeNo = tmpStoreNo;
                workstationNo = tmpWorkstationNo;
                comment = tmpComment;
            }

            protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
            {
                PropertyChangedEventHandler handler = PropertyChanged;

                if (handler != null)
                {
                    handler(this, new PropertyChangedEventArgs(propertyName));
                }
            }
            #endregion

            private bool Accept(LicenseRecordModel lic)
            {
                var tmp = list;
                tmp = list.Where(x => 
                chkProduct ? x.Product.Contains(product) : true &&
                chkClient ? x.Client.Contains(client) : true &&
                chkProduct ? x.Product.Contains(product) : true &&
                chkProduct ? x.Product.Contains(product) : true &&
                chkProduct ? x.Product.Contains(product) : true &&
                chkProduct ? x.Product.Contains(product) : true &&
                chkProduct ? x.Product.Contains(product) : true
                ).ToList();
                return false;
            }

            #region Commands
            public void Apply()
            {
                var tmp = new List<LicenseRecordModel>(list);
                dialogResult = new List<LicenseRecordModel>(list);
                string message = "";
                if (chkProduct)
                {
                    dialogResult =tmp.Where(x => x.Product.Contains(product.ToUpper())).ToList();
                    tmp = dialogResult;
                }
                if (chkClient)
                {
                    dialogResult = tmp.Where(x => x.Client.Contains(client.ToUpper())).ToList();
                    tmp = dialogResult;
                }
                if (chkDateAfter)
                {
                    DateTime after;
                    if (chkDateBefore)
                    {
                        DateTime before;
                        if (DateTime.TryParse(dateAfter, out after))
                        {
                            if (DateTime.TryParse(dateBefore, out before))
                            {
                                dialogResult = tmp.Where(x => DateTime.ParseExact(x.CreationDate, "yyyy-MM-dd", null) <= after && DateTime.ParseExact(x.CreationDate, "yyyy-MM-dd", null) >= before).ToList(); ;
                                tmp = dialogResult;
                            }
                            else message += "'Date Before' is not a valid date (yyyy-mm-dd)";
                        }
                        else message += "'Date After' is not a valid date (yyyy-mm-dd)";
                    }
                    else if (DateTime.TryParse(dateAfter, out after))
                    {
                        dialogResult = tmp.Where(x => DateTime.ParseExact(x.CreationDate, "yyyy-MM-dd", null) >= after).ToList();
                        tmp = dialogResult;
                    }
                    else message += "'Date After' is not a valid date (yyyy-mm-dd)";
                }
                if (chkDateBefore)
                {
                    DateTime before;
                    if (DateTime.TryParse(dateBefore, out before))
                    {
                        dialogResult = tmp.Where(x => DateTime.ParseExact(x.CreationDate, "yyyy-MM-dd", null) <= before).ToList();
                        tmp = dialogResult;
                    }
                    else message += "'Date After' is not a valid date (yyyy-mm-dd)";
                }
                if (chkSbsNo)
                {
                    dialogResult = tmp.Where(x => x.SbsNo.Contains(sbsNo)).ToList();
                    tmp = dialogResult;
                }
                if (chkStoreNo)
                {
                    dialogResult = tmp.Where(x => x.StoreNo.Contains(storeNo)).ToList();
                    tmp = dialogResult;
                }
                if (chkWorkstationNo)
                {
                    dialogResult = tmp.Where(x => x.WorkstationNo.Contains(workstationNo)).ToList();
                    tmp = dialogResult;
                }
                if (chkComment)
                {
                    dialogResult = tmp.Where(x => x.Comment.ToUpper().Contains(comment.ToUpper())).ToList();
                    tmp = dialogResult;
                }
                if (message != "")
                {
                    MessageBox.Show(message);
                }
                else
                {
                    DialogCloser.SetDialogResult(wnd, true);
                }
            }
            #endregion
        }

EDIT : Updated the View with bindings set to TwoWay (solved)

Phoque
  • 217
  • 3
  • 14
  • Also if using static class, you need to use `x:Static` for your bindings. – Trevor Jan 08 '20 at 13:30
  • the binding is done to the vm properties, then in the class I set the value in the static class – Phoque Jan 08 '20 at 13:32
  • You're setting all the `tmp` fields, shouldn't you be setting the actual properties? Look at the `public FilterWindowViewModel(IEnumerable source)` constructor... You really should have all props in your class you set. In the setter you can set your static fields as well. Your bindings are looking for these properties. Have you looked at your output for any binding errors by chance? – Trevor Jan 08 '20 at 13:50
  • Bindings work perfectly, the problem is that when I open the window a second time, I want to get the same values back, if I set them in the constructor as you told, it gets overwritten to empty strings when calling the ShowDialog() – Phoque Jan 08 '20 at 14:03
  • `Bindings work perfectly, the problem is that when I open the window a second time, I want to get the same values back` then you need to keep an instance, not static. – Trevor Jan 08 '20 at 14:05
  • I see, Create an instance of the ViewModel and then use this same instance every time I want to open my dialog window? Once again, the reset occurs when calling ShowDialog so this does not help actually – Phoque Jan 08 '20 at 14:10
  • Just retried, if I call the ShowDialog() multiple times by recreating the window and reassinging the same VM every time, I can see the value of my propertiesresetting when calling ShowDialog() in the inspector – Phoque Jan 08 '20 at 14:57
  • Binding source is the data source, the view model. Binding target is the receiver of the data, the `TextBox.Text` property. Setting the binding on the `TextBox.Text` property (binding target) to `OneWayToSource` (as you did) will only send data from the `TextBox.Text` to the binding view model (source <-- target). So the first time the binding is "activated" is when the `TextBox` was loaded. Hence it will send an empty string to the view model and override the default property values set by the `Helper`. – BionicCode Jan 14 '20 at 20:07
  • Since you want to send the `Helper` (view model default) values to the `TextBox.Text` the binding must be at least `OneWay` (source --> target). Because you also want to send input data from the `TextBox.Text` to the view model the binding becomes `TwoWay` (source <--> target). Since `TwoWay` is the default `Binding.Mode` for the `TextBox.Text` property you can safely remove the `Binding.Mode` from the binding expression: ``. – BionicCode Jan 14 '20 at 20:09
  • @BionicCode Did not know TwoWay was the default for Texbox.Text, I actually tried setting the bindings to TwoWay but with no luck (updated the post) – Phoque Jan 15 '20 at 09:38
  • Was is the context where you show the new `FilterWindowView`, `MainWindow`? – BionicCode Jan 15 '20 at 10:02
  • 1
    Why do you create a dialog `Window` of the class `FilterWindowView` but show us the XAML for `AddLicenseWindow`? – BionicCode Jan 15 '20 at 10:16
  • @BionicCode nice call, I messed it up the TwoWay Binding was only set in this other view not in the View I needed it in. Solved! – Phoque Jan 15 '20 at 10:19
  • I see. I added an answer to show how to fix and improve your code. You definitely don't need the `Helper`. – BionicCode Jan 15 '20 at 10:25
  • I added a link to my answer to show an example of data validation. This will add a clean structure to your view model. – BionicCode Jan 15 '20 at 10:38

2 Answers2

5

Several issues:

1) Since you want to send the Helper (view model default) values to the TextBox.Text the binding must be at least OneWay (source --> target). Because you also want to send input data from the TextBox.Text to the view model the binding becomes TwoWay (source <--> target).

Since TwoWay is the default Binding.Mode for the TextBox.Text property you can safely remove the Binding.Mode from the binding expression:

<TextBox Text="{Binding product, UpdateSourceTrigger=PropertyChanged}" />

2) You don't need the Helper class to store the data. Just add a property to the class that instantiates the dialog.
Also you don't need the RestoreCurrentFilters() method and the bunch of tmp... fields in your view model anymore.

public partial class MainWindow
{
  // Shared and reused view model instance
  private FilterWindowViewModel DialogViewModel{ get; set; }

  public MainWindow()
  { 
    this.DialogViewModel = new FilterWindowViewModel();
  }

  private void ShowDialog()
  {
    var dialog = new FilterWindowView() { DataContext = this.DialogViewModel };
    if (dialog.ShowDialog() ?? false)
    {
      //...
    }
  }
}

FilterWindowViewModel.cs

public class FilterWindowViewModel 
{
  private string _product;
  public string Product
  {
    get => _product; 
    set
    {
      if (_product == value)
        return;

      _product = value;

      this.chkProduct = !string.IsNullOrWhiteSpace(value);
      OnPropertyChanged();
    }
  }
}

3) Your view model should not have a reference to a Window.
4) Prefer data validation. Simply implement INotifyDataErrorInfo. Example

BionicCode
  • 1
  • 4
  • 28
  • 44
  • 1) This was the obvious part I was missing... ofr whatever reason I just couldn't see it missing 2) The RestoreCurrentFilters() and tmp fields was one try to solve the issue so yeah they will disappear now 3) The only way I found to call a Dialog window uses a static DialogCloser class which requires the VM to know about the View, asked for different solutions before but did not find any. 4) Don't know about this interface, will look into it – Phoque Jan 15 '20 at 10:41
  • So you are showing the dialog from another view model? Or why why do you think the static `DialogCloser` is the only way to show a dialog? – BionicCode Jan 15 '20 at 10:45
  • This is a [usage example](https://github.com/BionicCode/BionicUtilities.Net/blob/master/README.md#mvvm-dialog-attached-behavior) of a MVVM compliant customizable implementation of an attached behavior to show a dialog from a view model. You can get the library via [NuGet](https://www.nuget.org/packages/BionicUtilities.Net/). It uses an attached property which binds to a dialog view model. Setting this property will immediately show the dialog. By defining a `DataTemplate` that targets the type of the dialog view model you can customize the appearance. – BionicCode Jan 15 '20 at 11:09
  • Since you can pass an async callback delegate to the dialog view model which is automatically invoked once the dialog was closed, everything is asynchronous and fire-and-forget. – BionicCode Jan 15 '20 at 11:09
0

When you say 'when calling ShowDialog() the value is reset to ""', this is because the TextBox bindings use Mode=OneWayToSource. A change to the textbox text will update the VM property but not vice-versa. When the view is created, the textboxes won't be populated with the VM property values that you set in the constructor, so they will remain blank.

Additionally, these textbox values (empty strings) will actually update back into the VM properties when the view is first created, resulting in the behaviour you are seeing. Remove the binding mode from each textbox and it should work fine.

Andrew Stephens
  • 9,413
  • 6
  • 76
  • 152
  • Totally messed up there, you are right, this is what was missing, don't know how I could have not seen it for so long... – Phoque Jan 15 '20 at 10:20