MVVM IValueConverter Convert method getting empty string argument when expecting a float

倾然丶 夕夏残阳落幕 提交于 2020-01-02 19:52:28

问题


This may get a tad long, but here goes. I have created a small, wizard-style sample app using the MVVM pattern (basically a dumbed-down version of the code in my "real" app). In this app, the main window moves from through a List<..> of view models, with each view model displaying its associated view. I have 2 view model classes that are essentially identical, and they display the same view.

On the view is a combo box, populated with an array of float. The SelectedItem is bound to a float property on the view model. I have created a template for the combo box to display each item as a TextBlock, with the text taking the float value and going through a value converter.

The problem, when I switch back and forth between view models, all works fine as long as every view model I switch to is of the same class. As soon as I change the current page to an instance of a different view model, the value converter's Convert gets called with a 'value' parameter that is a zero-length string. Up til then, Convert was only being called with a float, as I would expect.

My question : why is the converter being called with the empty string ONLY in the case of switching view model classes?

I am attaching the main window XAML and view model, as well as the view/view models displayed for each "page". You'll notice that the main window view model has a list containing 2 instances of PageViewModel and 2 instances of OtherViewModel. I can switch back and forth between the first 2 fine, and the value converter only gets called with a float value. Once I switch to the first OtherViewModel instance, the converter gets an "extra" call with an empty string as the value.

Code snippets :

  1. MainWindow

    <Grid.Resources>
        <DataTemplate DataType="{x:Type local:PageViewModel}">
            <local:PageView />
        </DataTemplate>
        <DataTemplate DataType="{x:Type local:OtherViewModel}">
            <local:PageView />
        </DataTemplate>
    </Grid.Resources>
    
    <!-- Page -->
    <ContentControl Margin="5,5,5,35"
                    Height="100"
                    IsTabStop="False"
                    Content="{Binding CurrentPage}" />
    
    <!-- Commands -->
    <Button Margin="5,115,0,0" 
            Width="75"
            Content="&lt; Back"
            VerticalAlignment="Top"
            HorizontalAlignment="Left"
            Command="{Binding BackCommand}" />
    
    <Button Margin="85,115,0,0"
            Width="75"
            Content="Next &gt;"
            VerticalAlignment="Top"
            HorizontalAlignment="Left"
            Command="{Binding NextCommand}" />
    
  2. MainWindowViewModel

        public MainWindowViewModel()
        {
           m_pages = new List<BaseViewModel>();
           m_pages.Add(new PageViewModel(1, 7f));
           m_pages.Add(new PageViewModel(2, 8.5f));
           m_pages.Add(new OtherViewModel(3, 10f));
           m_pages.Add(new OtherViewModel(4, 11.5f));
           m_currentPage = m_pages.First();
    
           m_nextCommand = new BaseCommand(param => this.OnNext(), param => this.EnableNext());
           m_backCommand = new BaseCommand(param => this.OnBack(), param => this.EnableBack());
        }
    
        // Title
    
        public string Title
        {
           get
           {
              return (CurrentPage != null) ? CurrentPage.Name : Name;
           }
        }
    
        // Pages
    
        BaseViewModel m_currentPage = null;
        List<BaseViewModel> m_pages = null;
    
        public BaseViewModel CurrentPage
        {
           get
           {
              return m_currentPage;
           }
           set
           {
              if (value == m_currentPage)
                 return;
              m_currentPage = value;
              OnPropertyChanged("Title");
              OnPropertyChanged("CurrentPage");
           }
        }
    
        // Back
    
        ICommand m_backCommand = null;
        public ICommand BackCommand
        {
           get
           {
              return m_backCommand;
           }
        }
        public void OnBack()
        {
           CurrentPage = m_pages[m_pages.IndexOf(CurrentPage) - 1];
        }
        public bool EnableBack()
        {
           return CurrentPage != m_pages.First();
        }
    
        // Next
    
        ICommand m_nextCommand = null;
        public ICommand NextCommand
        {
           get
           {
              return m_nextCommand;
           }
        }
        public void OnNext()
        {
           CurrentPage = m_pages[m_pages.IndexOf(CurrentPage) + 1];
        }
        public bool EnableNext()
        {
           return CurrentPage != m_pages.Last();
        }
     }
    

Notice the 2 instance of one view model followed by 2 instances of the other.

  1. PageView

    <Grid.Resources>
        <x:Array x:Key="DepthList"
                 Type="sys:Single">
            <sys:Single>7</sys:Single>
            <sys:Single>8.5</sys:Single>
            <sys:Single>10</sys:Single>
            <sys:Single>11.5</sys:Single>
        </x:Array>
        <local:MyConverter x:Key="MyConverter" />
    </Grid.Resources>
    
    <TextBlock Text="Values:"
               Margin="5,5,0,0">
    </TextBlock>
    
    <ComboBox Width="100"
              Height="23"
              VerticalAlignment="Top"
              HorizontalAlignment="Left"
              Margin="5,25,0,0"
              DataContext="{Binding}"
              SelectedItem="{Binding Depth}"
              ItemsSource="{Binding Source={StaticResource DepthList}}">
        <ComboBox.ItemTemplate>
            <DataTemplate>
                <TextBlock Text="{Binding Converter={StaticResource MyConverter}}" />
            </DataTemplate>
        </ComboBox.ItemTemplate>
    </ComboBox>
    
  2. PageViewModel/OtherViewModel/MyConverter

       public class PageViewModel : BaseViewModel
       {
          public PageViewModel(int index, float depth)
          {
             Depth = depth;
             Name = "Page #" + index.ToString();
          }
    
          public float Depth
          {
             get;
             set;
          }
       }
    
       public class OtherViewModel : BaseViewModel
       {
          public OtherViewModel(int index, float depth)
          {
             Depth = depth;
             Name = "Other #" + index.ToString();
          }
    
          public float Depth
          {
             get;
             set;
          }
       }
    
       [ValueConversion(typeof(DateTime), typeof(String))]
       public class MyConverter : IValueConverter
       {
          public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
          {
             Debug.WriteLine("IValueConverter.Convert : received a " + value.GetType().Name);
    
             string text = "";
             if (value is float)
             {
                text = value.ToString();
             }
             else
             {
                throw new ArgumentException("MyConverter : input value is NOT a float.");
             }
    
             return text;
          }
    
          public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
          {
             return float.Parse(value as string);
          }
       }
    

Note: I can remove the exception in the Convert method, and everything seems to work fine. But, I would like to know why this is happening. Why is the converter getting an empty string instead of the expected float, and only when we switch view models?

Any insights would be greatly appreciated. Thanks in advance... Joe


回答1:


I've had the same issue (with an enum instead of a float).

When the View is closed the ComboBox Selection is emptied. You can check this by handling SelectionChanged event and inspecting the SelectionChangedEventArgs RemovedItems collection. This ends in String.Empty being passed into your ValueConverter.

In my case, I have modified the ValueConverter.Convert to allow string.Empty as a valid value, and return string.Empty. This is the code I used:

// When view is unloaded, ComboBox Selection is emptied and Convert is passed string.Empty 
// Hence we need to handle this conversion
if (value is string && string.IsNullOrEmpty((string)value))
{
    return string.Empty;
}



回答2:


Try

public BaseViewModel
{
   public virtual float Depth{get;set;}
   ...
}

Then

public class PageViewModel : BaseViewModel
{
   ...
   public override float Depth { get; set; }
}

and

public class OtherViewModel : BaseViewModel
{
   ...
   public override float Depth { get; set; }
}

Then you only need one DataTemplate

<Grid.Resources>
    <DataTemplate DataType="{x:Type local:BaseViewModel}">
        <local:PageView />
    </DataTemplate>
</Grid.Resources>

I'm guessing the strange value being passed to the converter is due to DataTemplates being switched.

Not tested



来源:https://stackoverflow.com/questions/9946104/mvvm-ivalueconverter-convert-method-getting-empty-string-argument-when-expecting

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!