xamarin 分解庞大的遗留ViewModels/Views并实现不变性

t1qtbnec  于 2023-06-27  发布在  其他
关注(0)|答案(2)|浏览(76)

在我们的应用程序中,有一些非常可怕的ViewModel和Fragments,现在我们需要重构。它们是巨大的,当然应该被分解。但是我并不清楚分解的正确方式(类和数据加载的顺序会导致绑定困难
p.s.约束框架是法律和习惯(

  • 之前:巨大而杂乱的类 *
public class HugeLegacyViewModel : ViewModelBase
{
    public TextViewModel FooText {get; set;}
    public ListViewModel FooList {get; set;} = new ListViewModel();

    public TextViewModel BarText {get; set;}
    public ListViewModel BarList {get; set;} = new ListViewModel();

    public HugeLegacyViewModel(IDependancy1 dependancy1)
    {
        ...
    }

    public void LoadData() // is called after object creation
    {
        var data = ...
        FooText = data.Foo.TextDto;
        FooList = data.Foo.ListDto.ToListViewModel();
        BarText = data.Bar.TextDto;
        BarList = data.Bar.ListDto.ToListViewModel();
    }
}

public class HugeLegacyFragment : FragmentBase<HugeLegacyViewModel>
{
    public void OnViewCreate(View view)
    {
        var fooText = view.FindLayoutById(Resources.Layout.fooTextLayout);
        var barText = view.FindLayoutById(Resources.Layout.barTextLayout);
        var fooList = view.FindLayoutById(Resources.Layout.fooListLayout);
        var barList = view.FindLayoutById(Resources.Layout.barListLayout);

        ShittyLegacyBinder.CreateBindings(
            () => fooText.Text == ViewModel.FooText.Text,
            () => fooText.Font == ViewModel.FooText.Font,
            () => barText.Text == ViewModel.BarText.Text,
            () => barText.Font == ViewModel.BarText.Font,

            () => fooList.List == ViewModel.FooList.List,
            () => fooList.Title == ViewModel.FooList.Title,
            () => barList.List == ViewModel.BarList.List,
            () => barList.Title == ViewModel.BarList.Title
        );
    }
}
  • AFTER:一组小类 *
public class FooViewModel : ViewModelBase
{
    public string Text {get;}
    public ReadOnlyCollection List {get;}
    ...
}

public class CoolNewViewModel : ViewModelBase
{
    public FooViewModel FooVm {get; private set;} = new FooViewModel();
    public BarViewModel BarVm {get; private set;} = new BarViewModel();

    public CoolNewViewModel(IDependancy1 dependancy1)
    {
        ...
    }

    public void LoadData()
    {
        var data = ...
        FooVm = data.Foo.ToVm();
        BarVm = data.Bar.ToVm();
    }
    ...
}

public class FooView : View
{
    private FooViewModel _vm;
    private TextView _textVew;
    private ListView _listView;

    public FooView(FooViewModel vm, TextView textView, ListView listView)
    {
        ...
        ShittyLegacyBinder.CreateBindings(
            () => _textVew == _vm.Text,
            () => _listView == _vm.List
        );
    }
}

public class CoolNewFragment : FragmentBase<HugeLegacyViewModel>
{
    private FooView? _foo;
    private BarView? _bar;

    public void OnViewCreate(View view) 
    // issue is here: OnViewCreate() is called eralier than LoadData() in vm,
    // so Views are now binded to instances that will be replaced in LoadData()
    //
    // bindings lead to stub empty classes
    {
        _foo = new FooView(
            vm: ViewModel.FooVm,
            textView: view.FindLayoutById(Resources.Layout.fooTextLayout),
            listView: view.FindLayoutById(Resources.Layout.fooListLayout)
        );
        _bar = new BarView(
            vm: ViewModel.BarVm,
            textView: view.FindLayoutById(Resources.Layout.barTextLayout),
            listView: view.FindLayoutById(Resources.Layout.barListLayout)
        );
    }
    ...
}

我想为新类的属性和字段实现不变性,但这相当具有挑战性
问题描述见上述评论:虽然属性应该是不可变的,但实际上在虚拟机中它们不能。数据在创建示例后加载,因此FooVm和BarVm将被覆盖。由于Fragment的代码在vm之前执行,因此覆盖后绑定将丢失(
下面提供了一些不那么好的解决方案:

  • 解决方案1:忘记不变性 *
public class FooViewModel1 : ViewModelBase
{
    public MutableString Text {get;}
    public Collection List {get;}

    public void ReplaceData(Foo data)
    {
        Text.String = data.Text;
        List.ReplaceAllWith(data.List);
    }
}

public class MediocreNewViewModel1 : ViewModelBase
{
    public FooViewModel FooVm {get; private set;} = new FooViewModel();
    public BarViewModel BarVm {get; private set;} = new BarViewModel();

    public MediocreNewViewModel1(IDependancy1 dependancy1)
    {
        ...
    }

    public void LoadData()
    {
        var data = ...
        FooVm.ReplaceData(data.Foo);
        BarVm.ReplaceData(data.Bar);
    }
    ...
}
  • 解决方案2:主虚拟机中的别名 *
public class FooViewModel2 : ViewModelBase
{
    public string Text {get;}
    public ReadOnlyCollection List {get;}
    ...
}

public class MediocreNewViewModel2 : ViewModelBase
{
    private FooViewModel _fooVm = new FooViewModel();
    private BarViewModel _barVm = new BarViewModel();

    public string FooText => _fooVm.Text;
    public string FooList => _fooVm.List;
    public string BarText => _barVm.Text;
    public string FooList => _barVm.List;

    public MediocreNewViewModel2(IDependancy1 dependancy1)
    {
        ...
    }

    public void LoadData()
    {
        var data = ...
        _fooVm = data.Foo.ToVm();
        _barVm = data.Bar.ToVm();
    }
    ...
}

public class FooView2 : View
{
    private TextView _textVew;
    private ListView _listView;

    public FooView2(string text, ReadOnlyCollection list TextView textView, ListView listView)
    {
        ...
        ShittyLegacyBinder.CreateBindings(
            () => _textVew == text,
            () => _listView == list
        );
    }
}

public class MediocreNewFragment2 : FragmentBase<HugeLegacyViewModel>
{
    private FooView2? _foo;
    private BarView2? _bar;

    public void OnViewCreate(View view) 
    {
        _foo = new FooView2(
            text: ViewModel.FooText,
            list: ViewModel.FooList,
            textView: view.FindLayoutById(Resources.Layout.fooTextLayout),
            listView: view.FindLayoutById(Resources.Layout.fooListLayout)
        );
        _bar = new BarView2(
            text: ViewModel.BarText,
            list: ViewModel.BarList,
            textView: view.FindLayoutById(Resources.Layout.barTextLayout),
            listView: view.FindLayoutById(Resources.Layout.barListLayout)
        );
    }
    ...
}
  • 解决方案3:将视图Map到实际Vm* 的 Package 器类
public class FooViewModel3 : ViewModelBase
{
    public string Text {get;}
    public ReadOnlyCollection List {get;}
    ...
}

public class FooViewModelWrapper
{
    private FooViewModel3 _vm;

    public string Text => _vm.Text;
    public ReadOnlyCollection List => _vm.List;

    public FooViewModelWrapper(FooViewModel3 vm)
    {
        ...
    }
}

public class MediocreNewViewModel3 : ViewModelBase
{
    private FooViewModel _fooVm = new FooViewModel();
    private BarViewModel _barVm = new BarViewModel();

    public FooViewModelWrapper FooVm {get; private set;} = new FooViewModelWrapper(_fooVm);
    public BarViewModelWrapper BarVm {get; private set;} = new BarViewModelWrapper(_barVm);

    public CoolNewViewModel(IDependancy1 dependancy1)
    {
        ...
    }

    public void LoadData()
    {
        var data = ...
        _fooVm = data.Foo.ToVm();
        _barVm = data.Bar.ToVm();
    }
    ...
}

public class FooView3 : View
{
    private FooViewModelWrapper _vm;
    private TextView _textVew;
    private ListView _listView;

    public FooView3(FooViewModelWrapper vm, TextView textView, ListView listView)
    {
        ...
        ShittyLegacyBinder.CreateBindings(
            () => _textVew == _vm.Text,
            () => _listView == _vm.List
        );
    }
}

public class MediocreNewFragment3 : FragmentBase<HugeLegacyViewModel>
{
    private FooView3? _foo;
    private BarView3? _bar;

    public void OnViewCreate(View view)
    {
        _foo = new FooView3(
            vm: ViewModel.FooVm,
            textView: view.FindLayoutById(Resources.Layout.fooTextLayout),
            listView: view.FindLayoutById(Resources.Layout.fooListLayout)
        );
        _bar = new BarView3(
            vm: ViewModel.BarVm,
            textView: view.FindLayoutById(Resources.Layout.barTextLayout),
            listView: view.FindLayoutById(Resources.Layout.barListLayout)
        );
    }
    ...
}

所以,问题:

有没有可能在没有妥协和样板代码的情况下重构它?

brvekthn

brvekthn1#

ViewModels和不变性

我想为新类的属性和字段实现不变性,但这相当具有挑战性
在.Net中,ViewModel通常是实现INotifyPropertyChanged接口的类。同样,ViewModel属性也会发生变化。(或者至少,变化不应该是一件令人惊讶的事情,也不应该是一件坏事。)它们可以是不可变的(当没有变化时),但这不是强制性的。
另一方面,想要实现不变性对于表示数据的**类来说完全有意义。
请记住,数据和视图模型是两个不同的概念。它们服务于不同的目的。

ViewModels类的目标:
  • 通过属性显示数据和命令。
  • 通知这些属性的任何更改。

所有这些都是为了支持绑定系统。

Data类目标:
  • 表示数据。

关于重构

据我所知你想要两样东西
1.通过引入更小和更专业的班级来减少一些大班的责任。(完美)
1.使更多的类不可变。(小心)
您所面临的技术挑战是在创建UI和关联的ViewModel之后加载实际数据。

解决此问题的方法是使用ViewModels,因为它们支持属性更改。

你可以***让更多的类成为不可变的,但是你至少需要1个 * 可变的 * ViewModel(来处理延迟到达的数据)。不可变的类是表示ViewModel公开的数据的类。

ua4mk5z4

ua4mk5z42#

后来我多次面对这个问题,并得出了这样的解决方案:
每个ViewModel都有一个对应的View和一个方法Bind(TViewModel vm)。一个视图<=>一个视图模型。
View只处理VM的绑定内容,其他什么都不做。如果parent有一个可以为空的内容,View会等待内容接收值,然后绑定它。如果View有其他子View,它只调用它们的Bind()方法,并让它们处理绑定。
这有助于加载数据:每个ViewModel都是有效的和“可绑定的”。如果它的一些内容没有加载,它只是空的,一旦加载-准备在视图中使用。
示例:

public class MainViewModel : ViewModel
{
    public record ContentViewModels(ChildViewModel1 Child1, ChildViewModel2 Child2);
    
    public ContentViewModels? Content { get; private set; }

    public async Task LoadContent()
    {
        Content = await _loader.LoadContent();
    }
}

public class MainView : BindableView<MainViewModel>
{
    private ChildView1 _childView1 = new();
    
    private ChildView2 _childView2 = new();
    
    public void Bind(MainViewModel vm)
    {
        new EBinding
        {
            //executes on every Content change
            () => BindVmContent(vm.Content), 
        };
    }
    
    private void BindVmContent(MainViewModel.ContentViewModels? content)
    {
        if (content is null)
            return;
        
        _childView1.Bind(content.Child1);
        _childView2.Bind(content.Child2);
    }
}

相关问题