简体   繁体   中英

Control Content and Property change

I have dialogbox with Content control with templates:

<ContentControl Content="{Binding Model,UpdateSourceTrigger=PropertyChanged}"  ContentTemplateSelector="{StaticResource TemplateSelector}"/>

and property change event at dialogbox context:

dialogContext.Model.PropertyChanged += (s, e) => Change(s,e, context);

private void Change(object s, PrropertyChangeEventArgs e, Context context)
{
  ...

  context.Mode = new Example()
  {
   ...
  }

  model.PropertyChanged += (sender, eventArgs) => 
          ModelChange(sender, eventArgs, context);
    context.Model = model;

}

I want to change some properties at model, that determine which custom template will be displayed. To reload new template and invoke temlate selector should I create new model and

add property change event to this. Is is ok, or is it another way to do this.

Update

The below implementation doesn't work because it turns out that the template selector is only reinvoked if the actual value of ContentControl.Content changes. If you've still got the same instance of Model, raising PropertyChanged will have no effect. I even tried overriding ModelClass.Equals() and ModelClass.GetHashCode() . Neither was called. Maybe the Binding is calling Object.ReferenceEquals() .

But I did find three ways to do this. All have been tested, now that I've learned my lesson.

If you're going to this much trouble to get a template selector to work, best to look for some other approach where you're not fighting the framework.

You could instead use style triggers to swap templates:

<ContentControl
    Content="{Binding Model}"
    >
    <ContentControl.Style>
        <Style TargetType="ContentControl">
            <Style.Triggers>
                <DataTrigger Binding="{Binding Model.Foo}" Value="foo">
                    <Setter 
                        Property="ContentTemplate" 
                        Value="{StaticResource Foo}" 
                        />
                </DataTrigger>
                <DataTrigger Binding="{Binding Model.Foo}" Value="bar">
                    <Setter 
                        Property="ContentTemplate" 
                        Value="{StaticResource Bar}" 
                        />
                </DataTrigger>
            </Style.Triggers>
        </Style>
    </ContentControl.Style>
</ContentControl>

...but the logic in your template selector may be quite a bit more complicated than that, in which case it may not be feasible.

Here's another. You don't need a template selector to select a template. A converter can return a DataTemplate too, and if you use a multi-binding converter, you can give it whatever it needs to look up a DataTemplate in the resources:

<ContentControl
    Content="{Binding Model}"
    >
    <ContentControl.ContentTemplate>
        <MultiBinding 
            Converter="{StaticResource ContentTemplateConverter}"
            >
            <!-- 
            We must bind to Model.Foo so the binding updates when that changes, 
            but we could also bind to Model as well if the converter wants to 
            look at other properties besides Foo. 
            -->
            <Binding Path="Model.Foo" />
            <!-- The ContentControl itself will be used for FindResource() -->
            <Binding RelativeSource="{RelativeSource Self}" />
        </MultiBinding>
    </ContentControl.ContentTemplate>
</ContentControl>

C#

public class ContentTemplateConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        var ctl = values[1] as FrameworkElement;

        switch ($"{values[0]}")
        {
            case "foo":
                return ctl.FindResource("Foo") as DataTemplate;
            case "bar":
                return ctl.FindResource("Bar") as DataTemplate;
        }
        return null;
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

One last possibility, and in my opinion the least, is to use the template selector, but make it work by actually replacing the value of Model every time one of its properties changes. Rewrite ModelClass so it can easily be cloned:

public ModelClass() {}
public ModelClass(ModelClass cloneMe) {
    this.Foo = cloneMe.Foo;
    this.Bar = cloneMe.Bar;
}

...and keep _model_PropertyChanged from my original answer, but change the guts so instead of merely raising PropertyChanged , it replaces the actual value of Model (which will of course still raise PropertyChanged , as a side effect):

private void _model_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
    if (e.PropertyName == nameof(ModelClass.Foo))
    {
        Model = new ModelClass(Model);
    }
}

I've tested that and while it's alarmingly goofy, it does work.

Instead of cloning ModelClass , you could use a "reference" class for the parent's Model property:

public class ModelClassRef {
    public ModelClassRef(ModelClass mc) { ... }
    public ModelClassRef { get; private set; }
}

But it's still wicked goofy. The viewmodel shouldn't "know" the view even exists, but here you are rewriting a chunk of it in a bizarre way just to work around a peculiarity in the implementation of a particular control. View workarounds belong in the view.


So when this.Model.Foo changes, you want to change the template? I would expect this to do the job:

#region Model Property
private ModelClass _model = null;
public ModelClass Model
{
    get { return _model; }
    set
    {
        if (value != _model)
        {
            if (_model != null)
            {
                _model.PropertyChanged -= _model_PropertyChanged;
            }
            _model = value;
            if (_model != null)
            {
                _model.PropertyChanged += _model_PropertyChanged;
            }
            OnPropertyChanged();
        }
    }
}

private void _model_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
    //  If Model.Foo changed, announce that Model changed. Any binding using 
    //  the Model property as its source will update, and that will cause 
    //  the template selector to be re-invoked. 

    if (e.PropertyName == nameof(ModelClass.Foo))
    {
        OnPropertyChanged(nameof(Model));
    }
}

This is defined in your viewmodel base class. Maybe you've already got essentially the same method and it's called something else; if so, use that one of course.

protected void OnPropertyChanged([CallerMemberName] String propName = null)
    => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propName));

By the way, get rid of UpdateSourceTrigger=PropertyChanged . ContentControl will never create a new value for its Content property and pass that back to your viewmodel through the binding. Can't, won't, and you wouldn't want it to. So you don't need to tell it exactly when to perform a task it's not capable of performing.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM