WPF Caliburn.Micro和带有UserControls的TabControl问题

14

我很确定这个问题已经有人回答过了,但我找不到它。

我正在尝试使用TabControl来切换用户控件(每个选项卡都不同,因此不使用Items)

以下是详细说明: 我有我的主视图和3个用户控件。Mainview有一个TabControl-每个选项卡应显示不同的用户控件。

我可以轻松地设置tabcontrol contect为usercontrol,但这样它就不与viewmodel绑定,只与视图绑定。

因此,我在我的VM中使用Conductor和ActivateItem。这里开始变得奇怪/令人沮丧。应用程序以Tab0选择启动,但Tab2(最后一个选项卡)内容。单击任何其他选项卡,加载该选项卡的正确视图模型。点击返回到Tab0,也会加载正确的内容。

我该如何停止这个?另外,我真的很希望切换选项卡时不会重新初始化视图模型,清除已经输入的字段。

无论如何,这是我的一些源代码,我要把它放在这里然后去处理其他事情,以免弄坏我的鼠标。

视图:

<TabControl HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Grid.Row ="1">
        <TabItem Header="PC Information">
            <Grid>
                <ContentControl x:Name="LoadRemoteInfo" cal:View.Model="{Binding ActiveItem}"/>
            </Grid>
        </TabItem>
        <TabItem Header="Remote Tools">
            <Grid>
                <ContentControl x:Name="LoadRemoteTools" cal:View.Model="{Binding ActiveItem}"/>
            </Grid>
        </TabItem>
        <TabItem Header="CHRemote">
            <Grid>
                <ContentControl x:Name="LoadCHRemote" cal:View.Model="{Binding ActiveItem}"/>
            </Grid>
        </TabItem>

    </TabControl>

以及 ViewModel:

class MainViewModel : Conductor<object>
{
    RemoteInfoViewModel remoteInfo = new RemoteInfoViewModel();
    RemoteToolsViewModel remoteTools = new RemoteToolsViewModel();
    CHRemoteViewModel chRemote = new CHRemoteViewModel();

    public MainViewModel()
    {
        ActivateItem(remoteInfo);
    }

    public void LoadRemoteInfo()
    {
        ActivateItem(remoteInfo);
    }

    public void LoadRemoteTools()
    {
        ActivateItem(remoteTools);
    }

    public void LoadCHRemote()
    {
        ActivateItem(chRemote);
    }
}
2个回答

34

我能建议一种稍微不同的方式吗?

这是我在主从场景中成功实践过的方法。假设您有一组子视图模型,我会为所有这些项准备一个标记接口,当然,如果存在涵盖所有子视图模型的方法,则可以添加您认为合适的属性/方法:

public interface IMainScreenTabItem : IScreen
{
}

您可以确定您想要所有的子模型都是Screen(或者在嵌套场景情况下,是Conductor)。这使得它们具有完整的初始化/激活/停用周期。

然后,子视图模型:

public sealed class ChRemoteViewModel : Screen, IMainScreenTabItem
{
    public ChRemoteViewModel()
    {
        DisplayName = "CH Remote";
    }
}

public sealed class PcInfoViewModel : Screen, IMainScreenTabItem
{
    public PcInfoViewModel()
    {
        DisplayName = "PC Info";
    }
}

public sealed class RemoteToolsViewModel : Screen, IMainScreenTabItem
{
    public RemoteToolsViewModel()
    {
        DisplayName = "Remote Tools";
    }
}

DisplayName将被显示为标题文本。将这些类设置为sealed是一个好的实践,因为DisplayName是一个虚拟属性,在不sealed的类的构造函数中调用虚拟方法是绝对不可取的。

然后,您可以添加相应的视图并设置您选择的IoC容器注册 - 您必须注册所有子视图模型作为实现IMainScreenTabItem的类,然后:

public class MainViewModel : Conductor<IMainScreenTabItem>.Collection.OneActive
{
    public MainViewModel(IEnumerable<IMainScreenTabItem> tabs)
    {
        Items.AddRange(tabs);
    }
}

MainView.xaml 文件仅包含以下内容:

<TabControl Name="Items"/>

它就可以直接使用了。如果您的子视图模型需要多个依赖项(例如,数据库访问、记录器、验证机制等),那么这也是一个非常好的便捷解决方案,现在您可以让IoC来完成所有繁重的工作,而不是手动实例化它们。

不过需要注意的是:选项卡将按照注入类的顺序放置。如果您希望控制顺序,可以在MainViewModel构造函数中通过传递自定义的IComparer<IMainScreenTabItem>或添加某些属性来对IMainScreenTabItem接口进行OrderBy或选择。默认选定项目将是Items列表中的第一个项目。

其他选项是使MainViewModel接受三个参数:

public MainViewModel(ChRemoteViewModel chRemoteViewModel, PcInfoViewModel pcInfo, RemoteToolsViewModel remoteTools)
{
    // Add the view models above to the `Items` collection in any order you see fit
}

尽管您可能会有超过2-3个子视图模型(很容易变得更多),但它将很快变得混乱。
关于“清除”部分。由IoC创建的视图模型符合常规生命周期:它们仅在初始化一次(OnInitialize),每次导航到其他地方时都将停用(OnDeactivate(bool))并在导航到它们时激活(OnActivate)。 OnDeactivate中的bool参数指示视图模型是否仅被停用或完全“关闭”(例如,当您关闭对话框窗口并导航离开时)。如果您完全关闭视图模型,则下次显示时将重新初始化。
这意味着任何绑定的数据将在OnActivate调用之间保留,并且您必须在OnDeactivate中明确清除它。更重要的是,如果您保留对子视图模型的强引用,则即使在您调用OnDeactivate(true)之后,数据也将在下一次初始化时仍然存在 - 这是因为IoC注入的视图模型只能创建一次(除非您将工厂函数以Func<YourViewModel>的形式注入), 然后根据需要进行初始化/激活/停用。

编辑

关于引导程序,我不太确定您使用的是哪种IoC容器。我的示例使用了SimpleInjector,但您也可以轻松地使用例如Autofac:
public class AppBootstrapper : Bootstrapper<MainViewModel>
{
    private Container container;

    /// <summary>
    /// Override to configure the framework and setup your IoC container.
    /// </summary>
    protected override void Configure()
    {
        container = new Container();
        container.Register<IWindowManager, WindowManager>();
        container.Register<IEventAggregator, EventAggregator>();
        var viewModels =
            Assembly.GetExecutingAssembly()
                .DefinedTypes.Where(x => x.GetInterface(typeof(IMainScreenTabItem).Name) != null && !x.IsAbstract && x.IsClass);
        container.RegisterAll(typeof(IMainScreenTabItem), viewModels);
        container.Verify();
    }

    /// <summary>
    /// Override this to provide an IoC specific implementation.
    /// </summary>
    /// <param name="service">The service to locate.</param><param name="key">The key to locate.</param>
    /// <returns>
    /// The located service.
    /// </returns>
    protected override object GetInstance(Type service, string key)
    {
        if (service == null)
        {
            var typeName = Assembly.GetExecutingAssembly().DefinedTypes.Where(x => x.Name.Contains(key)).Select(x => x.AssemblyQualifiedName).Single();

            service = Type.GetType(typeName);
        }
        return container.GetInstance(service);
    }

    protected override IEnumerable<object> GetAllInstances(Type service)
    {
        return container.GetAllInstances(service);
    }

    protected override void BuildUp(object instance)
    {
        container.InjectProperties(instance);
    }
}

请注意在Configure中注册viewModels

非常好的写作,认真地。我现在要实现它,然后告诉你结果如何。 - Para
嗯,我还在学习caliburn/mvvm,所以我有些不知所措 - 我需要在我的引导程序中做一些事情,只是不确定具体该做什么。你能否指点我一下方向?我尝试遵循https://caliburnmicro.codeplex.com/wikipage?title=Customizing%20The%20Bootstrapper,但没有成功。这让我感觉像个彻底的新手哈哈。 - Para
1
@JustinMangum,我已经添加了一个示例来解决你的问题。如果你正在使用MEF之类的东西,我可能无法提供太多帮助,因为它涉及到非常特定于IoC的方法,而我没有使用MEF的经验 :) - Patryk Ćwiek
SimpleInjector 运作良好。我得去研究一下 - 我以前也没有使用过 MEF,似乎无法让它喜欢上我。非常感谢您的帮助。之前,应用程序是可用的,只是不符合我的口味,我想学习如何正确地做这件事。就像我说的,我还是 MVVM 的新手(一直想学习它很长时间了),所以这真的帮助了我,因为引导程序是一个重要的步骤。 - Para
SimpleInjector代码示例在v3.0中已经无法使用。以下是修复方法:http://www.c-sharpcorner.com/blogs/migrating-to-simple-injector-30-with-caliburn-micro-bootstrap-changes - Eternal21
显示剩余3条评论

0

只是为了补充Patryk Ćwiek的精彩回答!

如果您已经在使用Caliburn Mirco并且不想添加更多的依赖项,您可以使用他们的SimpleContainer代替SimpleInjector或AutoFac。

就像这样注册实现:

container.AllTypesOf<IMainScreenTabItem>(Assembly.GetExecutingAssembly());

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接