close
دانلود آهنگ جدید
کار با Async/Await در Xamarin

یک توسعه دهنده .Net

کار با Async/Await در Xamarin

async/await,C#,xamarin,xamarin android,xamarin ios,xamarin forms,

برنامه نویسی ناهمگام (Asynchronous - Async) ... . استفاده از متدهای async  برای اجرای Task  های طولانی، مثل دانلود کردن داده ، به  پاسخگویی (responsive) رابط کاربری کمک میکند، در حالی که ار متدهای async  استفاده نمیکنید یا استفاده نادرست از async / await  کنید ، میتواند UI اپ شما تا پاسخ به ورودی کاربر تازمانی که اجرای طولانی task  کامل شود متوفق کند (یعنی تا task کانل اجرا بشوذ ui برنامه freez  میشود). که تجربه بدی برای کاربر خواهد داشت و باعث امتیاز پایین…

کار با Async/Await در Xamarin

کار با Async/Await در Xamarin 202

برنامه نویسی ناهمگام (Asynchronous - Async) ... . استفاده از متدهای async  برای اجرای Task  های طولانی، مثل دانلود کردن داده ، به  پاسخگویی (responsive) رابط کاربری کمک میکند، در حالی که ار متدهای async  استفاده نمیکنید یا استفاده نادرست از async / await  کنید ، میتواند UI اپ شما تا پاسخ به ورودی کاربر تازمانی که اجرای طولانی task  کامل شود متوفق کند (یعنی تا task کانل اجرا بشوذ ui برنامه freez  میشود). که تجربه بدی برای کاربر خواهد داشت و باعث امتیاز پایین برنامه در اپ استورها میشود. در این پست به نحوه استفاده async/await  و رفتار های غیرمنتظره در یک ListView  می پردازیم.


async/await  چیست ؟

کلمات کلیدی هستند که در .net 4.5 معرفی شدن که ساختن متد های async  را آسان و خوانایی کد را بالا میبرد.نحوه استفاده از async/await آسان است که در پشت صحنه از (Task Parallel Library) استفاده میکند.اگر قبل از .net 4.5  می خواستید یک task جدید را start (ایجاد کنید یا اجرا کنید) و در نخ UI بعد از اینکه task کامل شد کد را اجرا کنید، کد شما چیزی شبیه کد زیر میشد:






2
3
4
5
6
7
8
9
10
// Start a new task (this launches a new thread)
Task.Factory.StartNew (() => {
    // Do some work on a background thread, allowing the UI to remain responsive
    DoSomething();
// When the background work is done, continue with this code block
}).ContinueWith (task => {
    DoSomethingOnTheUIThread();
// the following forces the code in the ContinueWith block to be run on the
// calling thread, often the Main/UI thread.
}, TaskScheduler.FromCurrentSynchronizationContext ());



کد خیلی زیبایی نیست. حالا کد بالا با استفاده از async /await میشود:




2
await DoSomething();
DoSomethingOnTheUIThread();


 کد بالا در پشت صحنه به همان کد TPL کامپایل میشود مانند مثال اول میشود، همانطور که گفته شد استفاده از async/await  خیلی آسان است !



مشکلات استفاده از Async

درمطالعه درباره استفاده از async/await ممکن است این عبارت را دیده باشید "async all the way"  (شاید اصطلاح باشید -  همیشه async) ، اما این واقعا چه معنایی دارد؟نگران نباشید،به این معنی است که هر متدی که  یک متد async فراخوانی میکند (به عنوان مثال متدی که در امضاش کلمه کلیدی async دارد) زمان فراخوانی متد async  باید از کلمه کلیدی await استفاده کند.اگر از کلمه کلیدی await  هنگام فراخوانی یک متد async استفاده نشود میتواند منجر به پرتاپ استثنایی (exceptions thrown) که در زمان اجرا بلعیده میشود،که این امر باعث ایجاد مشکلاتی در برنامه و خطایابی میشود. استفاده از کلمه کلیدی await برای فراخوانی متد async اجباری میباشد همچنین استفاده از کلمه کلیدی async  در امضا متد نیز اجباریست. برای مثال:



async Task CallingMethod()
{
    var x = await MyMethodAsync();
}



این یک مشکل است اگر بخواهید یک متد async با استفاده از کلمه کلیدی await فراخوانی کنید ولی نتوانید از modifier (اصلاح کننده) async در متد فراخوانی شده  استفاده کنید، برای مثال اگر فراخوانی متد که این متد نمیتواند کلمه کلیدی async را در امضایش استفاده کرد یا یک سازنده است (constructor) یا یک متدی که توسط سیستم عامل (OS) فراخوانی میشود، مانند  GetView در  اندروید ArrayAdapter  یا  GetCell در IOS (سیتم عامل اپل) UITableViewDataSource . برای مثال:



public override View GetView(int position, View convertView, ViewGroup parent)
{
    // Can’t use await keyword in this method as you can’t use async keyword
    // in method signature due to the incompatible return type.
}



همونطور که میدانید، async متد یا void ، Task یا Task<T>  بر میگرداند،و بازگرداندن void باید فقط هنگام ایجاد یک event handler async  استفاده شود. در مورد متد GetView که در بالا ذکر شده، نیاز به برگرداندن یک اندروید View داریم، که به عنوان متد سیستم عامل فراخوانی می شود  نمیتواند به Task<View> تغییر و بازگردانیم ،بدیهی است که نمیشود از کلمه کلیدی await  استفاده کرد و بنابراین نمی تواند یک Task<T> را مدیریت کند و برگرداند.بدین ترتیب نمیتوانید کلمه کلیدی async رابه متد فوق (متد GetView) اضافه کنید و از این رو نمی توان از کلمه کلیدی await  هنگام فراخوانی متدی که async هست در متد فوق (متد GetView) استفاده کرد .


برای دور شدن از این، ممکن است وسوسه شوید،فقط فراخوانی متدی در  GetView (یا متد مشابهی که در آن امضا قابل تغییر نیست  بدون توجه به پلت فرم) به عنوان یک متد واسطه،و سپس فراخوانی کنید متد async  در متد واسط:




public override View GetView(int position, View convertView, ViewGroup parent)
{
    IntermediateMethod();
    // more code
}
 
async Task IntermediateMethod()
{
     await MyMethodAsync();
}




مشکل اینجاست که IntermediateMethod حالا یک async  متد است و بنابراین باید منتظر بمانیم درست مانند متد  MyMethodAsync لازم است که منتظر باشد. پس بنابراین شما هیچ چیزی بدست نیاوردید!IntermediateMethod نیزحالا یک async است و باید منتظر بمانیم(یعنی به کلمه کلیدی await نیاز دارد). علاوه بر این ، متد GetView به اجرا کردن تمام کد هایی که بعد از فراخوانی IntermediateMethod() ادامه میدهد، که ممکن است مطلوب باشد یا نباشد.اگر کد زیرین فراخوانی IntermediateMethod()   (کدهایی که بعد از فراخوانی IntermediateMethod() ) به نتیجه IntermediateMethod()  بستگی داشته باشد، آنگاه مطلوب نیست.در چنین سناریوهایی، ممکن است وسوسه شوید از متد Wait() (یا خاصیتResult ) روی  async task فراخوانی کنید. به عنوان مثال:




public override View GetView(int position, View convertView, ViewGroup parent)
{
    IntermediateMethod().Wait();
    // more code
}

صدا زدن Wait() روی متد async باعث صدا زدن نخ (thread) و توقف (pause)آن میشود تا زمانی که متد async  تکمیل شود. اگر این نخ UI باشد،مانند همین مورد (منظور متد GetView)،سپس UI در حالی که async task  اجرا میشود قطع خواهد شد.این خوب نیست، به خصوص در یک ArrayAdapter که داده ها را برای row های یک ListView تهیه میکند.کاربر قادر به تعامل با لیست ویو تازمانی که داده ها برای تمامی سطر ها دانلود نشود نخواهد بود،و scroll کردن لیست مشکل و یا کاملا غیر پاسخگو (freez می شود)خواهد بود،که اصلا تجربه خوبی برای کاربر نیست.همچنین خاصیت Result وجود دارد میتوانید روی متد async task فراخوانی کنید.Result موقعی استفاده میشود که اگر async task  شما داده ای را با استفاده از Task<T> به عنوان نوع بازگشتی متد async برگرداند.این نیز باعث صدا زدن نخ برای انتظار نتیجه async task  میشود:


public override View GetView(int position, View convertView, ViewGroup parent)
{
    view.Text = IntermediateMethod().Result;
    // more code
}
 
async Task<string> IntermediateMethod()
{
     return await MyMethodAsync(); // MyMethodAsync also returns Task<string> in this example
}



در حقیقت انجام کارهای بالا باعث هنگ کردن کامل UI برنامه و برای ListView  هرگز پر نشود، که یک non-starter (هیچ وقت شروع نمیشود).این ممکن فقط روند نا منظم داشته باشد:



بطورکلی، شما باید از استفاده کردن Wait() و Result اجتناب کنید، بخصوص روی نخ UI.

Using Async All the Way ( اصطلاح که میگه همیشه از async await  استفاده کنید)


پس چطور میتوانیم در این سناریو "async all the way"  دریافت کنیم؟
یکی از  راه های حل مشکل فوق این است که TPL قدیمی بازگردانده شود که اساس async/await تشکیل میدهد.شما به طور مستقیم از TPL استفاده میکنید،اما فقط یکبار برای شروع زنجیره ای از async  متد صدا میزنید(و بلافاصله یک نخ جدید شروع (start)  میشود).جایی در خط پایین، دوباره TPL به طور مستقیم استفاده میشود، همانطور که شما نیاز به استفاده از TPL برای شروع (start)  یک نخ جدید دارید(یعنی هر بار که از TPL به صورت مستقیم استفاده میکنیم یک نخ جدید شروع به کار کردن میکند).نمیتوانید یک نخ جدید را با استفاده از  کلمات کلیدی async/await شورع (start) کنید بعضی  از متد های پایین زنجیره باید نخ جدید را با TPL (یا مکانیزم دیگری) اجرا کنند.متد async که یک نخ جدید را راه اندازی میکند یک متد framework است، مانند یک .NET HttpClient async متد در بسیاری از موارد اگر بیشتر نباشد.اگر از متد های async framework  استفاده نکنید،برخی از متد های پایین زنجیره باید یک نخ جدید را راه اندازی (launch) کرده و Task یا Task<T> برگردانند.

بیایید با یک مثال  با استفاده از GetView  در یک پروژه اندروید شروع کنیم (مفهوم یکسانی برای پلت فرم های مختلف کار خواهد کر مثلا زامارین iOS یا زامارین فرم یا هرپلتفرم دیگری)بیایید بگوییم من یک ListView  دارم که میخواهم با دانلود text از اینترنت به صورت پویا (dynamically) پرش کنم(بیشتر احتمال دارد که اول لیست کل رشته ها را دانلود کنید و سپس row های لیست را با محتوای دانلود شده پر کنید ، ولی من اینجا برای نمایش هدف(هدف: استفاده async/await در متد غیر async) رشته ها را ردیف به ردیف دانلود و پر میکنیم،به علاوه مواردی وجود دارد که در آن ممکن است بخواهید این کار را به طریقی انجام دهید).من قطعا نمی خواهم نخ UI بلاک (block) شده و منتظر چندین بار دانلود بماند; بلکه می خواهم کاربر بتواند با ListView کار کند، Scroll کند،و هر متن (Text) که دانلود شده در هر cell ListView  ظاهر شود.همچنین می خواهم اطمینان حاصل کنم که اگر یک سلول از دید خارج شود،زمانی که استفاده مجدد از آن استفاده می شود بارگذاری (loading) متن را که در حال دانلود است لغو می کند و بارگذاری متن جدید آن ردیف را به جای آن آغاز می کند.ما این کار را با TPL و cancellation token ها انجام می دهیم. توضیح هر کد به صورت کامنت  بالا  هر کد وجود دارد.






2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
public override View GetView(int position, View convertView, ViewGroup parent)
{
    // We will need a CancellationTokenSource so we can cancel the async call
    // if the view moves back on screen while the text is already being loaded.
    // Without this, if a view is loading some text, but the view moves off and
    // back on screen, the new load may take less time than the old load and
    // then the old load will overwrite the new text load and the wrong data
    // will be displayed. So we will cancel any async task on a recycled view
    // before loading the new text.
    
    CancellationTokenSource cts;
 
    // re-use an existing view, if one is available
    View view = convertView; // re-use an existing view, if one is available
    
    // Otherwise create a new one
    if (view == null) {
        view = context.LayoutInflater.Inflate(Android.Resource.Layout.SimpleListItem1, null);
    }
    else
    {
        // If view exists, cancel any pending async text loading for this view
        // by calling cts.Cancel();
        var wrapper = view.Tag.JavaCast<Wrapper<CancellationTokenSource>>();
        cts = wrapper.Data;
 
        // If cancellation has not already been requested, cancel the async task
        if (!cts.IsCancellationRequested)
        {
           cts.Cancel();
        }
    }
 
    TextView textView = view.FindViewById<TextView>(Android.Resource.Id.Text1);
    textView.Text = "placeholder";
 
    // Create new CancellationTokenSource for this view's async call
    cts = new CancellationTokenSource();
 
    // Add it to the Tag property of the view wrapped in a Java.Lang.Object
    view.Tag = new Wrapper<CancellationTokenSource> { Data = cts };
 
    // Get the cancellation token to pass into the async method var ct = cts.Token;
 
    Task.Run(async () => {
        try
        {
            textView.Text = await GetTextAsync(position, ct);
        }
        catch (System.OperationCanceledException ex)
        {
            Console.WriteLine($"Text load cancelled: {ex.Message}");
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.Message);
        }
    }, ct);
 
    return view;
}


به طور خلاصه، متد بالا بررسی می کند که آیا این یک سلول مجدد (reused cell) است و اگر چنین باشد،اگر هنوز ناقص است، دانلود متن async موجود را لغو می کنیم.سپس  متن Placeholder را داخل cell لود (load) میکنیم، launch (راه اندازی) async task  برای دانلود متن صحیح برای row،و بلافاصله view با متن placeholder بر میگرداند، در نتیجه ListView  را پر میکند.این کار باعث میشود UI پاسخگو باشد و چیزی را در cell نشان میدهد درحالی که task راه اندازی شده و کار خود را برای دریافت متن صحیح از وب انجام میدهد. همانطور که متن دانلود شده دریافت می شود،خواهید دید placeholder ها یک به یک به متن دانلود شده تغییر میکنند(نه لزوم به ترتیب چئن زمان دانلود ها متفاوت است).من یک تاخیر تصادفی (random delay ) برای کار با Async task اضافه کردم تا این رفتار را شبیه سازی کنم چون من یک درخواست ساده و سریع را انجام می دهم.

در اینجا پیاده سازی GetTextAsync (...) قرار دارد:





async Task<string> GetTextAsync(int position, CancellationToken ct)
{
    // Check to see if a task was canceled; if so throw a canceled exception.
    // Good to check at several points, including just prior to returning the string.
    ct.ThrowIfCancellationRequested();
    
    // to simulate a task that takes variable amount of time
    await Task.Delay(rand.Next(100,500));
    ct.ThrowIfCancellationRequested();
    if (client == null)
        client = new HttpClient();
    string response = await client.GetStringAsync("http://example.com");
    string stringToDisplayInList = response.Substring(41, 14) + " " + position.ToString();
    ct.ThrowIfCancellationRequested();
    return stringToDisplayInList;
}
توجه داشته باشید  کامه کلیدی async  به صورت لامبدا به Task.Run() پاس دهم،به این ترتیب به من اجازه میدهد در صدا زدن async method ام از await  استفاده کنم، و در نتیجه دستیابی به   “async all the way” .دیگر ListView  نامنظمی در کار نیست !